feat: send balance reminder message to lark

This commit is contained in:
zihluwang committed 2025-06-29 10:36:02 +08:00
1 parent a229dd9789
commit 41bea2e59c
6 files changed
+133 -38

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import { generateSignature } from "../utils/signature-util"
export async function sendMessage(balance: number, env: Env) {
const larkBotId = env.LARK_BOT_ID
const larkSignKey = env.LARK_SIGN_KEY
if (!larkBotId) {
throw new Error("LARK_BOT_ID not set.")
}
const url = `https://open.larksuite.com/open-apis/bot/v2/hook/${larkBotId}`
const timestamp = Math.floor(Date.now() / 1000)
console.log(`timestamp = ${timestamp}`)
const signature = await generateSignature(timestamp, larkSignKey)
const data = {
msg_type: "text",
content: {
text: `Your UniAPI balance: US$ ${balance}`,
},
timestamp: timestamp,
sign: signature,
}
console.log(data)
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify(data)
})
if (!response.ok) {
const text = await response.text();
throw new Error(`Request failed: ${response.status} ${response.statusText}, ${text}`);
}
const result = await response.json();
console.log("Lark message send response:", result);
}
+24
View File
@@ -0,0 +1,24 @@
import { UniApiBalanceResponse } from "../types/uniapi-balance-response"
export async function getBalance(env: Env): Promise<number> {
const response = await fetch("https://api.uniapi.io/v1/billing/usage?unit=usd", {
headers: {
Authorization: `Bearer ${env.UNIAPI_TOKEN}`,
},
})
if (!response.ok) {
console.error(`Request failed, response status: ${response.status}`)
throw Error(`Request failed, response status: ${response.status}`)
}
const result = await response.json<UniApiBalanceResponse>()
const balance = result?.data?.balance
if (balance === undefined) {
console.error("No field balance.")
throw Error("No field balance.")
}
return balance
}
+14 -18
View File
@@ -1,22 +1,18 @@
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `npm run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `npm run deploy` to publish your worker
*
* Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the
* `Env` object can be regenerated with `npm run cf-typegen`.
*
* Learn more at https://developers.cloudflare.com/workers/
*/
import { getBalance } from "./api/uniapi"
import { sendMessage } from "./api/lark"
async function scheduleController(env: Env) {
// fetch balance from `UniAPI`
const balance = await getBalance(env)
console.log(`balance = ${balance}`)
// send message
await sendMessage(balance, env)
console.log("Message has sent.")
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const uniApiToken = env.UNIAPI_TOKEN;
const larkBotId = env.LARK_BOT_ID;
const larkSignKey = env.LARK_SIGN_KEY;
return new Response("Hello World!")
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
await scheduleController(env)
},
} satisfies ExportedHandler<Env>
+10
View File
@@ -0,0 +1,10 @@
interface ResponseData {
balance: number
used: number
cache_used: number
}
export interface UniApiBalanceResponse {
data: ResponseData
success: boolean
}
+33
View File
@@ -0,0 +1,33 @@
export async function generateSignature(timestamp: number, signKey: string): Promise<string> {
const stringToSign = `${timestamp}\n${signKey}`
const encoder = new TextEncoder()
const keyData = encoder.encode(stringToSign)
const messageData = new Uint8Array(0)
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{
name: "HMAC",
hash: "SHA-256",
},
false,
["sign"],
)
const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData)
return bufferToBase64(signatureBuffer)
}
function bufferToBase64(buffer: ArrayBuffer): string {
let binary = ""
const bytes = new Uint8Array(buffer)
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
+11 -20
View File
@@ -1,24 +1,15 @@
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import worker from '../src/index';
import { env, createExecutionContext, waitOnExecutionContext, SELF } from "cloudflare:test"
import { describe, it, expect } from "vitest"
import worker from "../src/index"
import { generateSignature } from "../src/utils/signature-util"
// For now, you'll need to do something like this to get a correctly-typed
// `Request` to pass to `worker.fetch()`.
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>
describe('Hello World worker', () => {
it('responds with Hello World! (unit style)', async () => {
const request = new IncomingRequest('http://example.com');
// Create an empty context to pass to `worker.fetch()`.
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
await waitOnExecutionContext(ctx);
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
});
it('responds with Hello World! (integration style)', async () => {
const response = await SELF.fetch('https://example.com');
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
});
});
describe("Hello World worker", () => {
it("responds with Hello World! (integration style)", async () => {
const response = await SELF.fetch("https://example.com")
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
})
})