@turingflow/agent-kit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +592 -2
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +31 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +105 -0
- package/dist/handlers.d.ts +92 -0
- package/dist/handlers.js +211 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/sdk.d.ts +106 -0
- package/dist/sdk.js +309 -0
- package/dist/server/agent.d.ts +27 -0
- package/dist/server/agent.js +31 -0
- package/dist/server/artifact-hooks.d.ts +6 -0
- package/dist/server/artifact-hooks.js +37 -0
- package/dist/server/artifact-tool-tracker.d.ts +24 -0
- package/dist/server/artifact-tool-tracker.js +347 -0
- package/dist/server/artifacts.d.ts +227 -0
- package/dist/server/artifacts.js +1061 -0
- package/dist/server/auth/config.d.ts +34 -0
- package/dist/server/auth/config.js +82 -0
- package/dist/server/auth/db.d.ts +159 -0
- package/dist/server/auth/db.js +680 -0
- package/dist/server/auth/handlers.d.ts +27 -0
- package/dist/server/auth/handlers.js +297 -0
- package/dist/server/auth/index.d.ts +15 -0
- package/dist/server/auth/index.js +14 -0
- package/dist/server/auth/login-options.d.ts +14 -0
- package/dist/server/auth/login-options.js +50 -0
- package/dist/server/auth/providers/dingtalk.d.ts +16 -0
- package/dist/server/auth/providers/dingtalk.js +177 -0
- package/dist/server/auth/providers/feishu.d.ts +15 -0
- package/dist/server/auth/providers/feishu.js +100 -0
- package/dist/server/auth/providers/index.d.ts +13 -0
- package/dist/server/auth/providers/index.js +24 -0
- package/dist/server/auth/session.d.ts +48 -0
- package/dist/server/auth/session.js +108 -0
- package/dist/server/auth/types.d.ts +118 -0
- package/dist/server/auth/types.js +7 -0
- package/dist/server/browser-agent-mcp.d.ts +67 -0
- package/dist/server/browser-agent-mcp.js +608 -0
- package/dist/server/cli.d.ts +6 -0
- package/dist/server/cli.js +112 -0
- package/dist/server/debug-log.d.ts +25 -0
- package/dist/server/debug-log.js +86 -0
- package/dist/server/handlers.d.ts +141 -0
- package/dist/server/handlers.js +523 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.js +28 -0
- package/dist/server/kit-api.d.ts +23 -0
- package/dist/server/kit-api.js +153 -0
- package/dist/server/model-sources.d.ts +32 -0
- package/dist/server/model-sources.js +58 -0
- package/dist/server/oa.d.ts +87 -0
- package/dist/server/oa.js +397 -0
- package/dist/server/providers/openrouter.d.ts +51 -0
- package/dist/server/providers/openrouter.js +283 -0
- package/dist/server/render-markdown.d.ts +2 -0
- package/dist/server/render-markdown.js +50 -0
- package/dist/server/sdk.d.ts +269 -0
- package/dist/server/sdk.js +2025 -0
- package/dist/server/session-access.d.ts +5 -0
- package/dist/server/session-access.js +68 -0
- package/dist/server/session-fork.d.ts +10 -0
- package/dist/server/session-fork.js +142 -0
- package/dist/server/session-store.d.ts +56 -0
- package/dist/server/session-store.js +148 -0
- package/dist/server/sessions.d.ts +24 -0
- package/dist/server/sessions.js +541 -0
- package/dist/server/share.d.ts +15 -0
- package/dist/server/share.js +433 -0
- package/dist/server/tool-activity.d.ts +2 -0
- package/dist/server/tool-activity.js +26 -0
- package/dist/server/transcription/xiaomi-mimo.d.ts +8 -0
- package/dist/server/transcription/xiaomi-mimo.js +86 -0
- package/dist/server/upload.d.ts +13 -0
- package/dist/server/upload.js +81 -0
- package/dist/server/user-files.d.ts +28 -0
- package/dist/server/user-files.js +311 -0
- package/dist/server/voice.d.ts +25 -0
- package/dist/server/voice.js +61 -0
- package/dist/server/web-fetch.d.ts +71 -0
- package/dist/server/web-fetch.js +330 -0
- package/dist/styles.css +3354 -0
- package/dist/transcription/xiaomi-mimo.d.ts +8 -0
- package/dist/transcription/xiaomi-mimo.js +86 -0
- package/dist/ui/index.d.ts +358 -0
- package/dist/ui/index.js +7730 -0
- package/dist/upload.d.ts +13 -0
- package/dist/upload.js +81 -0
- package/dist/voice.d.ts +25 -0
- package/dist/voice.js +61 -0
- package/package.json +74 -4
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-kit · OA 开放平台客户端(钉钉 / 飞书)
|
|
3
|
+
*
|
|
4
|
+
* 用途:会话分享「发送到 OA」链路。
|
|
5
|
+
* - 钉钉:前端在钉钉客户端内用 JSAPI 选择会话(chooseConversationByCorpId)拿到一次性 cid,
|
|
6
|
+
* 服务端用企业内部应用 access_token 调「发送普通消息」message/send_to_conversation;
|
|
7
|
+
* - 飞书:前端在飞书客户端内用 H5 JSAPI chooseChat 拿到 open_chat_id,
|
|
8
|
+
* 服务端用 tenant_access_token 调 im/v1/messages 发送文本。
|
|
9
|
+
*
|
|
10
|
+
* 凭证来自认证 env(DD_CLIENT_ID/DD_CLIENT_SECRET、FS_APP_ID/FS_APP_SECRET),
|
|
11
|
+
* access_token / jsapi_ticket 进程级内存缓存(接近过期时提前 5 分钟刷新)。
|
|
12
|
+
*
|
|
13
|
+
* 签名:钉钉/飞书 H5 JSAPI 前端鉴权均为
|
|
14
|
+
* signature = sha1(jsapi_ticket + nonceStr + timeStamp + url)
|
|
15
|
+
* (拼接顺序固定,见两家开放平台 JSAPI 鉴权文档)。
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
// ----------------------------- 内存缓存 -----------------------------
|
|
19
|
+
const CACHE_TTL_MS = 100 * 60 * 1000; // 100 分钟(token/ticket 有效期 2 小时,提前 20 分钟刷新)
|
|
20
|
+
const cache = new Map();
|
|
21
|
+
function cacheGet(key) {
|
|
22
|
+
const item = cache.get(key);
|
|
23
|
+
if (!item)
|
|
24
|
+
return null;
|
|
25
|
+
if (Date.now() >= item.expiresAt) {
|
|
26
|
+
cache.delete(key);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return item.value;
|
|
30
|
+
}
|
|
31
|
+
function cacheSet(key, value, ttlMs = CACHE_TTL_MS) {
|
|
32
|
+
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
33
|
+
}
|
|
34
|
+
/** 简单 JSON 请求封装:非 2xx 抛错(带响应体摘要)。 */
|
|
35
|
+
async function apiFetchJson(url, init) {
|
|
36
|
+
const res = await fetch(url, init);
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
let data = null;
|
|
39
|
+
try {
|
|
40
|
+
data = text ? JSON.parse(text) : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`OA 接口返回非 JSON(${res.status}):${text.slice(0, 200)}`);
|
|
44
|
+
}
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const msg = (data && (data.message || data.msg || data.errmsg)) || `HTTP ${res.status}`;
|
|
47
|
+
throw new Error(`OA 接口错误:${msg}`);
|
|
48
|
+
}
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
// ----------------------------- 钉钉 -----------------------------
|
|
52
|
+
/** 钉钉企业内部应用 access_token(gettoken)。 */
|
|
53
|
+
export async function getDingTalkToken(appKey, appSecret) {
|
|
54
|
+
const key = `dd:token:${appKey}`;
|
|
55
|
+
const cached = cacheGet(key);
|
|
56
|
+
if (cached)
|
|
57
|
+
return cached;
|
|
58
|
+
const data = await apiFetchJson(`https://oapi.dingtalk.com/gettoken?appkey=${encodeURIComponent(appKey)}&appsecret=${encodeURIComponent(appSecret)}`, { method: "GET" });
|
|
59
|
+
if (!data.access_token)
|
|
60
|
+
throw new Error(`钉钉 gettoken 失败:${data.errmsg || "无 access_token"}`);
|
|
61
|
+
cacheSet(key, data.access_token);
|
|
62
|
+
return data.access_token;
|
|
63
|
+
}
|
|
64
|
+
/** 钉钉 jsapi_ticket(H5 JSAPI 前端鉴权用)。 */
|
|
65
|
+
export async function getDingTalkJsapiTicket(appKey, appSecret) {
|
|
66
|
+
const key = `dd:ticket:${appKey}`;
|
|
67
|
+
const cached = cacheGet(key);
|
|
68
|
+
if (cached)
|
|
69
|
+
return cached;
|
|
70
|
+
const token = await getDingTalkToken(appKey, appSecret);
|
|
71
|
+
const data = await apiFetchJson(`https://oapi.dingtalk.com/get_jsapi_ticket?access_token=${encodeURIComponent(token)}`, { method: "GET" });
|
|
72
|
+
if (!data.ticket)
|
|
73
|
+
throw new Error(`钉钉 get_jsapi_ticket 失败:${data.errmsg || "无 ticket"}`);
|
|
74
|
+
cacheSet(key, data.ticket);
|
|
75
|
+
return data.ticket;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 根据 unionId 解析企业内 userid(OAuth 登录未落 user_code 时,分享发送链路兜底用)。
|
|
79
|
+
* 接口:POST /topapi/v2/user/getbyunionid(文档另列旧路径 /topapi/user/getbyunionid,失败自动回退)。
|
|
80
|
+
* 权限:成员信息读权限;缺权限或解析失败返回空串,由调用方决定如何报错。
|
|
81
|
+
*/
|
|
82
|
+
export async function resolveDingTalkUserIdByUnionId(appKey, appSecret, unionId) {
|
|
83
|
+
if (!unionId)
|
|
84
|
+
return "";
|
|
85
|
+
const token = await getDingTalkToken(appKey, appSecret);
|
|
86
|
+
const body = JSON.stringify({ unionid: unionId });
|
|
87
|
+
for (const path of ["topapi/v2/user/getbyunionid", "topapi/user/getbyunionid"]) {
|
|
88
|
+
try {
|
|
89
|
+
const data = await apiFetchJson(`https://oapi.dingtalk.com/${path}?access_token=${encodeURIComponent(token)}`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body,
|
|
93
|
+
});
|
|
94
|
+
const code = String(data.errcode ?? data.errcode);
|
|
95
|
+
if (code === "0" || code === "0.0") {
|
|
96
|
+
const userid = data.result?.userid;
|
|
97
|
+
if (typeof userid === "string" && userid)
|
|
98
|
+
return userid;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// 换路径重试
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
/** 钉钉 H5 JSAPI dd.config 签名参数(企业内部应用 · 官方新版算法)。
|
|
108
|
+
*
|
|
109
|
+
* 重要(2025+ 新版网页应用/H5 微应用,见开放平台《JSAPI鉴权》/《开发网页应用前端》):
|
|
110
|
+
* plain = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "×tamp=" + timeStamp + "&url=" + decodeUrl(url)
|
|
111
|
+
* signature = SHA256(plain)
|
|
112
|
+
* 旧版微应用是 sha1(ticket+nonceStr+timeStamp+url) 纯拼接,新版必须用上面的键值对 + SHA-256,
|
|
113
|
+
* 否则钉钉端重算签名不一致 → dd.config 报 errorCode 9「签名校验失败」(参数全一致也会失败)。
|
|
114
|
+
*/
|
|
115
|
+
export async function dingTalkJsapiConfig(appKey, appSecret, corpId, agentId, url) {
|
|
116
|
+
const ticket = await getDingTalkJsapiTicket(appKey, appSecret);
|
|
117
|
+
const timeStamp = String(Date.now());
|
|
118
|
+
const nonceStr = randomStr(16);
|
|
119
|
+
const plain = `jsapi_ticket=${ticket}&noncestr=${nonceStr}×tamp=${timeStamp}&url=${decodeUrl(url)}`;
|
|
120
|
+
// 诊断日志:钉钉 dd.config 报「签名校验失败」时,用这里的参数与钉钉报错逐字段比对
|
|
121
|
+
//(重点看 url 是否与钉钉记录的页面 url 完全一致——大小写/末尾斜杠/query/端口)
|
|
122
|
+
console.log(`[agent-kit oa] dingtalk jsapi-sign: ticket=${ticket.slice(0, 8)}… nonce=${nonceStr} ` +
|
|
123
|
+
`timestamp=${timeStamp} url=${url} agentId=${agentId} corpId=${corpId.slice(0, 8)}…`);
|
|
124
|
+
return {
|
|
125
|
+
agentId,
|
|
126
|
+
corpId,
|
|
127
|
+
timeStamp,
|
|
128
|
+
nonceStr,
|
|
129
|
+
signature: sha256(plain),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 官方 decodeUrl:iOS 端传入的 url 是 encode 过的、Android 是原始 url,
|
|
134
|
+
* 统一按「协议 + 主机 + 路径 + (query 做 URL 解码)」重建,避免编码不一致导致签名校验失败。
|
|
135
|
+
* 与官方 Java 示例(URL.getProtocol/getAuthority/getPath/getQuery + URLDecoder)等价。
|
|
136
|
+
*/
|
|
137
|
+
function decodeUrl(url) {
|
|
138
|
+
try {
|
|
139
|
+
const u = new URL(url);
|
|
140
|
+
let out = `${u.protocol}//${u.host}${u.pathname}`;
|
|
141
|
+
if (u.search)
|
|
142
|
+
out += `?${decodeURIComponent(u.search.slice(1))}`;
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 钉钉「发送普通消息」:向 JSAPI 选中的一次性会话 cid 发消息。
|
|
151
|
+
* opts.url 提供时发 markdown 消息(title + 链接),钉钉客户端渲染为带预览的链接卡片;
|
|
152
|
+
* 否则回退纯文本(兼容旧调用)。
|
|
153
|
+
*/
|
|
154
|
+
export async function sendDingTalkMessage(appKey, appSecret, cid, sender, text, opts) {
|
|
155
|
+
const token = await getDingTalkToken(appKey, appSecret);
|
|
156
|
+
let msg;
|
|
157
|
+
if (opts?.url) {
|
|
158
|
+
// markdown 消息:title 显示为消息头部,正文里的 [text](url) 是点击即打开的链接(带预览)
|
|
159
|
+
msg = {
|
|
160
|
+
msgtype: "markdown",
|
|
161
|
+
markdown: {
|
|
162
|
+
title: (opts.title || "AI 对话分享").slice(0, 60),
|
|
163
|
+
text: `### ${opts.title || "AI 对话分享"}\n${text.slice(0, 1500)}\n\n[打开对话](${opts.url})`,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
msg = { msgtype: "text", text: { content: text.slice(0, 2000) } };
|
|
169
|
+
}
|
|
170
|
+
const data = await apiFetchJson(`https://oapi.dingtalk.com/message/send_to_conversation?access_token=${encodeURIComponent(token)}`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "Content-Type": "application/json" },
|
|
173
|
+
body: JSON.stringify({ sender, cid, msg }),
|
|
174
|
+
});
|
|
175
|
+
// 该接口成功时 errcode 可能为字符串 "0"
|
|
176
|
+
const code = String(data.errcode ?? data.errcode);
|
|
177
|
+
if (code !== "0" && code !== "0.0") {
|
|
178
|
+
throw new Error(`钉钉发送失败:${data.errmsg || data.message || JSON.stringify(data).slice(0, 200)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ----------------------------- 飞书 -----------------------------
|
|
182
|
+
/** 飞书 tenant_access_token(企业内部应用)。 */
|
|
183
|
+
export async function getFeishuTenantToken(appId, appSecret, baseUrl) {
|
|
184
|
+
const key = `fs:token:${appId}`;
|
|
185
|
+
const cached = cacheGet(key);
|
|
186
|
+
if (cached)
|
|
187
|
+
return cached;
|
|
188
|
+
const data = await apiFetchJson(`${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/json" },
|
|
191
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
192
|
+
});
|
|
193
|
+
if (!data.tenant_access_token)
|
|
194
|
+
throw new Error(`飞书 tenant_access_token 失败:${data.msg || "无 token"}`);
|
|
195
|
+
const expireSec = Number(data.expire || 7200);
|
|
196
|
+
cacheSet(key, data.tenant_access_token, (expireSec - 300) * 1000);
|
|
197
|
+
return data.tenant_access_token;
|
|
198
|
+
}
|
|
199
|
+
/** 飞书 jsapi_ticket(H5 JSAPI 前端鉴权用)。 */
|
|
200
|
+
export async function getFeishuJsapiTicket(appId, appSecret, baseUrl) {
|
|
201
|
+
const key = `fs:ticket:${appId}`;
|
|
202
|
+
const cached = cacheGet(key);
|
|
203
|
+
if (cached)
|
|
204
|
+
return cached;
|
|
205
|
+
const token = await getFeishuTenantToken(appId, appSecret, baseUrl);
|
|
206
|
+
const data = await apiFetchJson(`${baseUrl}/open-apis/jssdk/ticket/get`, {
|
|
207
|
+
method: "GET",
|
|
208
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
209
|
+
});
|
|
210
|
+
// 兼容两种返回结构:官方为 { code, msg, data: { ticket, expire_in } },部分网关可能平铺到顶层
|
|
211
|
+
const ticket = (data?.data && typeof data.data.ticket === "string" && data.data.ticket) ||
|
|
212
|
+
(typeof data.ticket === "string" ? data.ticket : "");
|
|
213
|
+
if (!ticket)
|
|
214
|
+
throw new Error(`飞书 jssdk ticket 失败:${data.msg || "无 ticket"}`);
|
|
215
|
+
cacheSet(key, ticket);
|
|
216
|
+
return ticket;
|
|
217
|
+
}
|
|
218
|
+
/** 飞书 H5 JSAPI(window.h5sdk.config)签名参数。 */
|
|
219
|
+
export async function feishuJsapiConfig(appId, appSecret, baseUrl, url) {
|
|
220
|
+
const ticket = await getFeishuJsapiTicket(appId, appSecret, baseUrl);
|
|
221
|
+
const timestamp = Date.now();
|
|
222
|
+
const nonceStr = randomStr(16);
|
|
223
|
+
// 飞书拼接规则(与钉钉不同!):key=value& 键值对格式,字段名小写,
|
|
224
|
+
// 顺序固定 jsapi_ticket → noncestr → timestamp → url,url 不含 # 后部分(保留 query)。
|
|
225
|
+
const plain = `jsapi_ticket=${ticket}&noncestr=${nonceStr}×tamp=${timestamp}&url=${url}`;
|
|
226
|
+
return { appId, timestamp, nonceStr, signature: sha1(plain) };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 飞书「发送消息」到会话(chat_id)。
|
|
230
|
+
* opts.url 提供时发 link 消息(title/text/message_link),飞书客户端渲染为链接卡片预览;
|
|
231
|
+
* 否则回退文本消息(兼容旧调用)。
|
|
232
|
+
*/
|
|
233
|
+
export async function sendFeishuMessage(appId, appSecret, baseUrl, chatId, text, opts) {
|
|
234
|
+
const token = await getFeishuTenantToken(appId, appSecret, baseUrl);
|
|
235
|
+
let msgType;
|
|
236
|
+
let content;
|
|
237
|
+
if (opts?.url) {
|
|
238
|
+
msgType = "link";
|
|
239
|
+
content = JSON.stringify({
|
|
240
|
+
title: (opts.title || "AI 对话分享").slice(0, 120),
|
|
241
|
+
text: text.slice(0, 500),
|
|
242
|
+
message_link: opts.url,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
msgType = "text";
|
|
247
|
+
content = JSON.stringify({ text: text.slice(0, 2000) });
|
|
248
|
+
}
|
|
249
|
+
const data = await apiFetchJson(`${baseUrl}/open-apis/im/v1/messages?receive_id_type=chat_id`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: {
|
|
252
|
+
"Content-Type": "application/json",
|
|
253
|
+
Authorization: `Bearer ${token}`,
|
|
254
|
+
},
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
receive_id: chatId,
|
|
257
|
+
msg_type: msgType,
|
|
258
|
+
content,
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
// 成功时 code 恒为数字 0;缺失或非 0 视为失败(不能用 !data.code:!0 === true)
|
|
262
|
+
if (data.code !== 0) {
|
|
263
|
+
throw new Error(`飞书发送失败:${data.msg || JSON.stringify(data).slice(0, 200)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 飞书「以当前用户身份」发送消息(user_access_token)。
|
|
268
|
+
* 不要求应用机器人加入目标会话(用户本就在自己的会话里);
|
|
269
|
+
* 需应用开通 im:message + im:message.send_as_user(用户身份读发消息)且 OAuth 授权 scope 含 im:message。
|
|
270
|
+
* 注意:im/v1/messages 不支持 msg_type=link(历史 API 才有),带链接时发 interactive 卡片:
|
|
271
|
+
* 大卡片 + 标题竖线 + 直接展示聊天摘要 + 整卡点击跳转(豆包分享同款样式)。
|
|
272
|
+
*/
|
|
273
|
+
export async function sendFeishuMessageAsUser(userAccessToken, baseUrl, chatId, text, opts) {
|
|
274
|
+
let msgType;
|
|
275
|
+
let content;
|
|
276
|
+
if (opts?.url) {
|
|
277
|
+
msgType = "interactive";
|
|
278
|
+
const summary = escapeCardMarkdown(opts.chatSummary || text.slice(0, 500));
|
|
279
|
+
const title = escapeCardMarkdown((opts.title || "AI 对话分享").slice(0, 60));
|
|
280
|
+
// 卡片 JSON 1.0:card_link 是「卡片整体点击跳转链接」(官方能力,1.0/2.0 均支持),
|
|
281
|
+
// 点击卡片任意区域即跳转分享页;div 元素的 url 属性在部分客户端不生效,不再使用
|
|
282
|
+
const card = {
|
|
283
|
+
config: { wide_screen_mode: true },
|
|
284
|
+
card_link: { url: opts.url },
|
|
285
|
+
elements: [
|
|
286
|
+
// 标题:左侧竖线 + 加粗标题(豆包分享同款 border-left 样式)
|
|
287
|
+
{ tag: "div", text: { tag: "lark_md", content: `**▎${title}**` } },
|
|
288
|
+
// 聊天摘要:直接展示会话内容
|
|
289
|
+
{ tag: "div", text: { tag: "lark_md", content: summary.slice(0, 1500) } },
|
|
290
|
+
// 底部提示(非链接样式):点击卡片任意处跳转
|
|
291
|
+
{ tag: "div", text: { tag: "lark_md", content: "— 点击查看完整对话 —" } },
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
content = JSON.stringify(card);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
msgType = "text";
|
|
298
|
+
content = JSON.stringify({ text: text.slice(0, 2000) });
|
|
299
|
+
}
|
|
300
|
+
const data = await apiFetchJson(`${baseUrl}/open-apis/im/v1/messages?receive_id_type=chat_id`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
"Content-Type": "application/json",
|
|
304
|
+
Authorization: `Bearer ${userAccessToken}`,
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
receive_id: chatId,
|
|
308
|
+
msg_type: msgType,
|
|
309
|
+
content,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
if (data.code !== 0) {
|
|
313
|
+
throw new Error(`飞书发送失败:${data.msg || JSON.stringify(data).slice(0, 200)}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 飞书 user_access_token 刷新(OAuth offline_access scope)。
|
|
318
|
+
* 返回 { accessToken, refreshToken };refreshToken 为空时表示飞书未返回新刷新令牌(保留旧值)。
|
|
319
|
+
*/
|
|
320
|
+
export async function refreshFeishuUserToken(appId, appSecret, baseUrl, refreshToken) {
|
|
321
|
+
const appAccessToken = await getFeishuAppAccessToken(appId, appSecret, baseUrl);
|
|
322
|
+
const data = await apiFetchJson(`${baseUrl}/open-apis/authen/v1/refresh_access_token`, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { "Content-Type": "application/json" },
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
grant_type: "refresh_token",
|
|
327
|
+
refresh_token: refreshToken,
|
|
328
|
+
app_access_token: appAccessToken,
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
if (data.code !== 0 || !data.data?.access_token) {
|
|
332
|
+
throw new Error(`飞书 user_access_token 刷新失败:${data.msg || JSON.stringify(data).slice(0, 200)}`);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
accessToken: String(data.data.access_token),
|
|
336
|
+
refreshToken: typeof data.data.refresh_token === "string" ? data.data.refresh_token : null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/** 飞书 app_access_token(内部应用;OAuth token 刷新等场景用)。 */
|
|
340
|
+
export async function getFeishuAppAccessToken(appId, appSecret, baseUrl) {
|
|
341
|
+
const key = `fs:app-token:${appId}`;
|
|
342
|
+
const cached = cacheGet(key);
|
|
343
|
+
if (cached)
|
|
344
|
+
return cached;
|
|
345
|
+
const data = await apiFetchJson(`${baseUrl}/open-apis/auth/v3/app_access_token/internal`, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: { "Content-Type": "application/json" },
|
|
348
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
349
|
+
});
|
|
350
|
+
const token = data.app_access_token || data.data?.app_access_token;
|
|
351
|
+
if (!token)
|
|
352
|
+
throw new Error(`飞书 app_access_token 失败:${data.msg || "无 token"}`);
|
|
353
|
+
cacheSet(key, token, 6900_000);
|
|
354
|
+
return token;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* 钉钉机器人发送群聊消息(企业内部应用机器人)。
|
|
358
|
+
* POST /v1.0/robot/groupMessages/send —— 接受 chooseChat 选会话返回的 openConversationId(旧接口
|
|
359
|
+
* send_to_conversation 只认旧格式 cid 且"不再支持新应用接入",故改用本接口)。
|
|
360
|
+
* 前置条件:应用已申请「企业内机器人发送消息权限」,且机器人已加入目标群。
|
|
361
|
+
* robotCode 未显式配置时用应用 AppKey(企业内部应用机器人 code = AppKey)。
|
|
362
|
+
*/
|
|
363
|
+
export async function sendDingTalkRobotGroupMessage(appKey, appSecret, robotCode, openConversationId, markdown) {
|
|
364
|
+
const token = await getDingTalkToken(appKey, appSecret);
|
|
365
|
+
await apiFetchJson(`https://api.dingtalk.com/v1.0/robot/groupMessages/send`, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
"x-acs-dingtalk-access-token": token,
|
|
370
|
+
},
|
|
371
|
+
body: JSON.stringify({
|
|
372
|
+
msgKey: "sampleMarkdown",
|
|
373
|
+
msgParam: JSON.stringify(markdown),
|
|
374
|
+
openConversationId,
|
|
375
|
+
robotCode,
|
|
376
|
+
}),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// ----------------------------- 工具 -----------------------------
|
|
380
|
+
function randomStr(len) {
|
|
381
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
382
|
+
let out = "";
|
|
383
|
+
const bytes = createHash("sha256").update(`${Date.now()}:${Math.random()}`).digest();
|
|
384
|
+
for (let i = 0; i < len; i++)
|
|
385
|
+
out += chars[bytes[i % bytes.length] % chars.length];
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
/** 转义卡片 markdown 里的特殊字符,避免聊天内容破坏卡片渲染。 */
|
|
389
|
+
function escapeCardMarkdown(input) {
|
|
390
|
+
return input.replace(/([\\`*_[\]()#~|>+\-!])/g, "\\$1");
|
|
391
|
+
}
|
|
392
|
+
function sha1(input) {
|
|
393
|
+
return createHash("sha1").update(input, "utf8").digest("hex");
|
|
394
|
+
}
|
|
395
|
+
function sha256(input) {
|
|
396
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
397
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-kit · OpenAI 兼容外部 API 通道(OpenRouter / DeepSeek / 通义等)
|
|
3
|
+
*
|
|
4
|
+
* 通过环境变量 AGENT_PROVIDER=openrouter 切换(见 sdk.ts 通道路由)。
|
|
5
|
+
* 环境变量(通用 OpenAI 兼容命名,任意满足 OpenAI chat/completions 格式的厂商通用):
|
|
6
|
+
* OPENAI_API_KEY 必填。API 密钥(如 OpenRouter / DeepSeek / 通义千问的 key)
|
|
7
|
+
* OPENAI_BASE_URL 必填。端点根,如 https://openrouter.ai/api/v1 或 https://api.deepseek.com
|
|
8
|
+
* OPENAI_MODEL 必填。模型 id,如 inclusionai/ling-3.0-flash-sante:free 或 deepseek-v4-flash
|
|
9
|
+
* 旧命名 OPENROUTER_API_KEY / OPENROUTER_BASE_URL / OPENROUTER_MODEL 保留兼容回退(已废弃)。
|
|
10
|
+
*
|
|
11
|
+
* 能力边界(v1):
|
|
12
|
+
* - 纯对话(无 agent 工具循环):system + 最后一条 user 消息;
|
|
13
|
+
* - 流式与非流式两种出口,SSE 事件协议与 CodeBuddy 通道一致(text / done / error);
|
|
14
|
+
* - 不支持图片、不支持 thinking 增量。
|
|
15
|
+
*/
|
|
16
|
+
/** OpenAI 兼容通道配置(可从 SdkConfig 透传,也可走环境变量) */
|
|
17
|
+
export interface OpenRouterProviderConfig {
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** 模型 id,默认 OPENAI_MODEL env(兼容 OPENROUTER_MODEL) */
|
|
20
|
+
model?: string;
|
|
21
|
+
/** 端点根,默认 OPENAI_BASE_URL env(兼容 OPENROUTER_BASE_URL) */
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
/** 追加的系统提示词(服务端业务 systemPrompt) */
|
|
24
|
+
systemPrompt?: string;
|
|
25
|
+
}
|
|
26
|
+
/** 上游错误(带 HTTP 状态码与面向用户的友好信息) */
|
|
27
|
+
export declare class OpenRouterError extends Error {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
constructor(message: string, status: number);
|
|
30
|
+
}
|
|
31
|
+
/** 面向用户的友好兜底文案(与 CodeBuddy 通道 AGENT_SERVICE_UNAVAILABLE 风格一致) */
|
|
32
|
+
export declare const OPENROUTER_UNAVAILABLE = "\n\n\uFF08OpenRouter \u901A\u9053\u5F02\u5E38\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u8054\u7CFB\u7BA1\u7406\u5458\u3002\uFF09";
|
|
33
|
+
/**
|
|
34
|
+
* 一次性对话(非流式):返回模型最终文本。
|
|
35
|
+
* 失败抛 OpenRouterError(HTTP 状态 + 友好信息)。
|
|
36
|
+
*/
|
|
37
|
+
export declare function runOpenRouterQuery(prompt: string, config?: OpenRouterProviderConfig): Promise<string>;
|
|
38
|
+
/**
|
|
39
|
+
* 对话流(SSE 事件流),事件协议与 CodeBuddy 通道一致:
|
|
40
|
+
* event: text data: {"text":"增量"} 模型正文增量
|
|
41
|
+
* event: done data: {"text":""} 整轮结束
|
|
42
|
+
* event: error data: {"text":"友好提示"} 上游异常(随后关闭)
|
|
43
|
+
*/
|
|
44
|
+
export declare function createOpenRouterStream({ prompt, systemPrompt, extraSystem, signal, config, }: {
|
|
45
|
+
prompt: string;
|
|
46
|
+
systemPrompt?: string;
|
|
47
|
+
/** 业务侧额外 system(来自 messages 中 role=system 的拼接,与 CodeBuddy 通道同语义) */
|
|
48
|
+
extraSystem?: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
config?: OpenRouterProviderConfig;
|
|
51
|
+
}): ReadableStream<Uint8Array>;
|