@qzsy/dsh-bridge 0.0.0-stage → 2.18.0
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/CHANGELOG.md +1107 -0
- package/LICENSE +21 -0
- package/README.en.md +488 -0
- package/README.md +509 -2
- package/client/build.mjs +43 -0
- package/client/client.js +9864 -0
- package/client/host-families.js +442 -0
- package/client/index.js +6883 -0
- package/client/locales.mjs +242 -0
- package/client/mobile-styles.js +1299 -0
- package/client/mobile-yield.js +219 -0
- package/client/panel-locales.mjs +777 -0
- package/client/picker-yield.js +77 -0
- package/client/resource-url-compat.js +90 -0
- package/client/settings-nav-icon.js +105 -0
- package/client/theme-tokens.js +49 -0
- package/client/tunnel-guard.js +46 -0
- package/client/unlock-manager.js +144 -0
- package/cordis.patch.yml +4 -0
- package/lib/auth/dsh-native-cookie.js +148 -0
- package/lib/auth/login-template.js +516 -0
- package/lib/auth/manager.js +596 -0
- package/lib/bridge-rpc-constants.js +66 -0
- package/lib/bridge-rpc.js +722 -0
- package/lib/cloudflared-manager.mjs +1277 -0
- package/lib/cloudflared-network-args.js +99 -0
- package/lib/cloudflared-process-scan.js +153 -0
- package/lib/compat.js +172 -0
- package/lib/connection-compat.js +115 -0
- package/lib/feishu/comment.js +50 -0
- package/lib/feishu/gateway.js +644 -0
- package/lib/feishu/index.js +432 -0
- package/lib/feishu/lark-bundled.mjs +131929 -0
- package/lib/feishu/markdown.js +148 -0
- package/lib/feishu/node.js +1152 -0
- package/lib/feishu/slash-commands.js +255 -0
- package/lib/index.js +3554 -0
- package/lib/platform/account-ops.js +248 -0
- package/lib/platform/activity.js +169 -0
- package/lib/platform/ask-user.js +293 -0
- package/lib/platform/base.js +147 -0
- package/lib/platform/changes-digest.js +93 -0
- package/lib/platform/commands.js +267 -0
- package/lib/platform/config-restore.js +78 -0
- package/lib/platform/conversation-bridge.js +1544 -0
- package/lib/platform/dsh-storage.js +222 -0
- package/lib/platform/index.js +10 -0
- package/lib/platform/instances.js +458 -0
- package/lib/platform/manager.js +130 -0
- package/lib/platform/message-split.js +229 -0
- package/lib/platform/session-catalog.js +405 -0
- package/lib/platform/session-config.js +52 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/gateway.js +1107 -0
- package/lib/qq/index.js +561 -0
- package/lib/qq/node.js +1153 -0
- package/lib/qq/qqbot-bundled.mjs +8273 -0
- package/lib/restart-helper.mjs +133 -0
- package/lib/saas/device-auth.mjs +366 -0
- package/lib/saas/tunnel-client.mjs +695 -0
- package/lib/security/log-sanitize.js +50 -0
- package/lib/security/path-validator.js +195 -0
- package/lib/security/rate-limiter.js +93 -0
- package/lib/session-strip.js +100 -0
- package/lib/sse-keepalive.js +115 -0
- package/lib/telegram/gateway.js +643 -0
- package/lib/telegram/index.js +255 -0
- package/lib/telegram/node.js +410 -0
- package/lib/tunnel-client.mjs +537 -0
- package/lib/wechat/gateway.js +1000 -0
- package/lib/wechat/index.js +282 -0
- package/lib/wechat/media.js +302 -0
- package/lib/wechat/node.js +360 -0
- package/llms.txt +36 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -4
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// dsh-bridge 飞书 Slash Command(斜杠指令面板)同步
|
|
2
|
+
//
|
|
3
|
+
// 官方文档:docs/dev/lark-sdk/official-slash-command.md("飞书智能体支持 Slash Command")
|
|
4
|
+
//
|
|
5
|
+
// 该能力未包含在任何官方 Node SDK 中(@larksuite/channel / @larksuiteoapi/node-sdk 均无对应方法),
|
|
6
|
+
// 因此这里按官方文档直连 HTTP:管理接口为 application v7 的复数路径
|
|
7
|
+
// POST /open-apis/application/v7/app_slash_commands 创建(需 application:app_slash_command:write)
|
|
8
|
+
// GET /open-apis/application/v7/app_slash_commands 列表(需 ...:read)
|
|
9
|
+
// PATCH /open-apis/application/v7/app_slash_commands/:id 更新
|
|
10
|
+
// DELETE /open-apis/application/v7/app_slash_commands/:id 删除
|
|
11
|
+
//
|
|
12
|
+
// 已知约束(文档要点):客户端需 PC ≥7.70 / 移动端 ≥7.71 才显示面板;每应用上限 100 条;
|
|
13
|
+
// command 名称唯一(重名创建返回 code 40000000 + "command already exists");删除后同名可重建;
|
|
14
|
+
// 生效延迟为服务端约 5 分钟 + 客户端缓存约 3 分钟。
|
|
15
|
+
|
|
16
|
+
export const SLASH_COMMAND_API_PATH = '/open-apis/application/v7/app_slash_commands'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 图标统一使用官方默认值:实测(2026-10 生产验证)创建/更新时传入文档清单内的其他 icon_key
|
|
20
|
+
* 都会被服务端静默忽略——PATCH 返回 code 0,但 GET 列表里始终是 `skill_outlined`。
|
|
21
|
+
* 因此这里不写自定义图标,避免"每次同步都判定为需要更新"的无效 PATCH。
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_ICON = 'skill_outlined'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 期望注册的斜杠指令:与 lib/platform/commands.js 的文本指令一一对应(command 字段不带前导斜杠)。
|
|
27
|
+
*/
|
|
28
|
+
export const DESIRED_SLASH_COMMANDS = [
|
|
29
|
+
{ command: 'help', icon: DEFAULT_ICON, zh: '查看可用指令与用法', en: 'Show available commands' },
|
|
30
|
+
{ command: 'new', icon: DEFAULT_ICON, zh: '新建会话并开始任务(可跟提示词)', en: 'Start a new session' },
|
|
31
|
+
{ command: 'sessions', icon: DEFAULT_ICON, zh: '列出历史会话', en: 'List sessions' },
|
|
32
|
+
{ command: 'use', icon: DEFAULT_ICON, zh: '切换到指定编号的会话', en: 'Switch session by index' },
|
|
33
|
+
{ command: 'rename', icon: DEFAULT_ICON, zh: '重命名当前会话', en: 'Rename current session' },
|
|
34
|
+
{ command: 'workspaces', icon: DEFAULT_ICON, zh: '查看已注册工作区', en: 'List workspaces' },
|
|
35
|
+
{ command: 'addworkspace', icon: DEFAULT_ICON, zh: '添加工作区目录', en: 'Add a workspace directory' },
|
|
36
|
+
{ command: 'status', icon: DEFAULT_ICON, zh: '查看 Agent 运行状态', en: 'Show agent status' },
|
|
37
|
+
{ command: 'stop', icon: DEFAULT_ICON, zh: '停止当前正在执行的任务', en: 'Stop the running task' },
|
|
38
|
+
{ command: 'end', icon: DEFAULT_ICON, zh: '结束当前会话', en: 'End the current session' },
|
|
39
|
+
{ command: 'diff', icon: 'update-ai_outlined', zh: '导出本轮改动的 .patch 补丁', en: "Export this turn's changes as a .patch" },
|
|
40
|
+
{ command: 'ask', icon: DEFAULT_ICON, zh: '回答 Agent 的提问(/ask 1 选第 1 项)', en: 'Answer the Agent\'s question' },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/** 权限类错误码(文档 "典型错误码"):视为"应用没开通斜杠指令权限",不再重试 */
|
|
44
|
+
const PERMISSION_ERROR_CODES = new Set([99991640, 99991661, 99991663])
|
|
45
|
+
|
|
46
|
+
function isBizError(body) {
|
|
47
|
+
return Boolean(body) && typeof body.code === 'number' && body.code !== 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function errorInfo(body, command) {
|
|
51
|
+
return {
|
|
52
|
+
code: typeof body?.code === 'number' ? body.code : undefined,
|
|
53
|
+
message: String(body?.msg ?? body?.message ?? body ?? 'unknown error'),
|
|
54
|
+
command: command ?? undefined,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isAlreadyExists(body) {
|
|
59
|
+
return /already exists/i.test(String(body?.msg ?? ''))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isNotFound(body) {
|
|
63
|
+
return /not found/i.test(String(body?.msg ?? ''))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 归一化已记录的指令映射:字符串视为 { id, ours: false }(保守:不认为是自己创建的,永不删除)。
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeManagedIds(managedIds = {}) {
|
|
70
|
+
const out = {}
|
|
71
|
+
for (const [command, value] of Object.entries(managedIds ?? {})) {
|
|
72
|
+
if (!value) continue
|
|
73
|
+
if (typeof value === 'string') out[command] = { id: value, ours: false }
|
|
74
|
+
else if (typeof value === 'object' && value.id) out[command] = { id: String(value.id), ours: value.ours === true }
|
|
75
|
+
}
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isPermissionErrorCode(code) {
|
|
80
|
+
return PERMISSION_ERROR_CODES.has(Number(code))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 创建/更新的请求体(icon 按文档放在 description 内) */
|
|
84
|
+
export function buildSlashCommandPayload(spec) {
|
|
85
|
+
return {
|
|
86
|
+
command: spec.command,
|
|
87
|
+
description: {
|
|
88
|
+
default_value: spec.zh,
|
|
89
|
+
i18n: { zh_cn: spec.zh, en_us: spec.en },
|
|
90
|
+
icon: { icon_key: spec.icon },
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 列表项与期望是否一致(列表响应把 icon 放在顶层,创建请求放在 description 内,两处都读)。
|
|
97
|
+
* 图标也参与比较:服务端当前只回默认图标,传自定义图标会被忽略——若不比较,就会每次同步都白 PATCH。
|
|
98
|
+
*/
|
|
99
|
+
export function slashCommandMatches(existing, spec) {
|
|
100
|
+
const description = existing?.description ?? {}
|
|
101
|
+
const i18n = description.i18n ?? {}
|
|
102
|
+
const iconKey = existing?.icon?.icon_key ?? description.icon?.icon_key ?? ''
|
|
103
|
+
return String(description.default_value ?? '') === spec.zh
|
|
104
|
+
&& String(i18n.zh_cn ?? '') === spec.zh
|
|
105
|
+
&& String(i18n.en_us ?? '') === spec.en
|
|
106
|
+
&& iconKey === spec.icon
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 统一读取业务响应体:SDK 的默认 HTTP 实例已由拦截器剥离到业务体,自定义实例可能包一层 data */
|
|
110
|
+
function readBody(res) {
|
|
111
|
+
if (isBizError(res)) return res
|
|
112
|
+
if (res?.data && typeof res.data.code === 'number') return res.data
|
|
113
|
+
return res
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function callApi(client, token, { method, path, data }) {
|
|
117
|
+
const headers = { Authorization: `Bearer ${token}` }
|
|
118
|
+
if (data !== undefined) headers['Content-Type'] = 'application/json; charset=utf-8'
|
|
119
|
+
const url = `${client.domain}${path}`
|
|
120
|
+
try {
|
|
121
|
+
const res = await client.httpInstance.request({ url, method, headers, data })
|
|
122
|
+
return readBody(res)
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// 非 2xx(如 400 缺 Token / Token 无效)由 axios 抛出,业务体在 err.response.data
|
|
125
|
+
const body = err?.response?.data
|
|
126
|
+
if (body) return readBody(body)
|
|
127
|
+
throw err
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 同步斜杠指令到期望清单(幂等):
|
|
133
|
+
* - 缺失 → 创建(记为 ours: true)
|
|
134
|
+
* - 存在但描述/图标不一致 → 更新(保留原归属:同名同名指令若本非本插件创建,记为 ours: false)
|
|
135
|
+
* - 存在且一致 → 采用(不写)
|
|
136
|
+
* - 仅【本插件创建过(ours: true)】且已不在清单内 → 删除
|
|
137
|
+
*
|
|
138
|
+
* managedIds 形如 { [command]: { id, ours } };字符串形式按 { id, ours: false } 保守处理(只读不删)。
|
|
139
|
+
*
|
|
140
|
+
* @param {object} opts
|
|
141
|
+
* @param {object} opts.client 底层 node-sdk Client(channel.rawClient)
|
|
142
|
+
* @param {() => Promise<string>} opts.getToken 取 tenant_access_token(调用方负责缓存)
|
|
143
|
+
* @param {object} [opts.logger]
|
|
144
|
+
* @param {Array} [opts.desired] 期望清单
|
|
145
|
+
* @param {object} [opts.managedIds] 之前记录的 { command: { id, ours } }
|
|
146
|
+
* @returns {Promise<{ok:boolean, ids:object, created:string[], updated:string[], adopted:string[], deleted:string[], error?:object}>}
|
|
147
|
+
*/
|
|
148
|
+
export async function syncSlashCommands({
|
|
149
|
+
client,
|
|
150
|
+
getToken,
|
|
151
|
+
logger,
|
|
152
|
+
desired = DESIRED_SLASH_COMMANDS,
|
|
153
|
+
managedIds = {},
|
|
154
|
+
} = {}) {
|
|
155
|
+
const result = { ok: false, ids: normalizeManagedIds(managedIds), created: [], updated: [], adopted: [], deleted: [], error: null }
|
|
156
|
+
const ids = normalizeManagedIds(managedIds)
|
|
157
|
+
const desiredNames = new Set(desired.map((item) => item.command))
|
|
158
|
+
|
|
159
|
+
let token
|
|
160
|
+
try {
|
|
161
|
+
token = await getToken()
|
|
162
|
+
} catch (err) {
|
|
163
|
+
result.error = errorInfo(err)
|
|
164
|
+
return result
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
return await syncWithToken({ client, token, desired, managedIds, result, ids, desiredNames })
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// 网络异常等非业务错误:保留已完成进度,交由调用方决定是否重试
|
|
171
|
+
result.ids = ids
|
|
172
|
+
result.error = errorInfo(err)
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function syncWithToken({ client, token, desired, managedIds, result, ids, desiredNames }) {
|
|
178
|
+
const listRes = await callApi(client, token, { method: 'GET', path: SLASH_COMMAND_API_PATH })
|
|
179
|
+
if (isBizError(listRes)) {
|
|
180
|
+
result.error = errorInfo(listRes)
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
const items = Array.isArray(listRes?.data?.items) ? listRes.data.items : []
|
|
184
|
+
const byName = new Map(items.filter((item) => item?.command).map((item) => [item.command, item]))
|
|
185
|
+
|
|
186
|
+
for (const spec of desired) {
|
|
187
|
+
const found = byName.get(spec.command)
|
|
188
|
+
if (found?.command_id) {
|
|
189
|
+
// 归属沿用既有记录(此前创建过 → ours 保持 true;否则视为他人/手工创建,ours: false)
|
|
190
|
+
ids[spec.command] = { id: found.command_id, ours: ids[spec.command]?.ours === true }
|
|
191
|
+
if (slashCommandMatches(found, spec)) {
|
|
192
|
+
result.adopted.push(spec.command)
|
|
193
|
+
} else {
|
|
194
|
+
const res = await callApi(client, token, {
|
|
195
|
+
method: 'PATCH',
|
|
196
|
+
path: `${SLASH_COMMAND_API_PATH}/${found.command_id}`,
|
|
197
|
+
data: buildSlashCommandPayload(spec),
|
|
198
|
+
})
|
|
199
|
+
if (isBizError(res)) {
|
|
200
|
+
result.ids = ids
|
|
201
|
+
result.error = errorInfo(res, spec.command)
|
|
202
|
+
return result
|
|
203
|
+
}
|
|
204
|
+
result.updated.push(spec.command)
|
|
205
|
+
}
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const res = await callApi(client, token, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
path: SLASH_COMMAND_API_PATH,
|
|
212
|
+
data: buildSlashCommandPayload(spec),
|
|
213
|
+
})
|
|
214
|
+
if (isBizError(res)) {
|
|
215
|
+
// 并发或历史残留:服务端对 command 有唯一约束 → 重新拉列表认领其 command_id
|
|
216
|
+
if (isAlreadyExists(res)) {
|
|
217
|
+
const relist = await callApi(client, token, { method: 'GET', path: SLASH_COMMAND_API_PATH })
|
|
218
|
+
const item = (relist?.data?.items ?? []).find((it) => it?.command === spec.command)
|
|
219
|
+
if (item?.command_id) {
|
|
220
|
+
ids[spec.command] = { id: item.command_id, ours: false }
|
|
221
|
+
result.adopted.push(spec.command)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
result.ids = ids
|
|
226
|
+
result.error = errorInfo(res, spec.command)
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
const created = res?.data?.command_id
|
|
230
|
+
if (created) ids[spec.command] = { id: created, ours: true }
|
|
231
|
+
result.created.push(spec.command)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 清理:仅删除【本插件创建过(ours: true)】且已不在期望清单中的指令
|
|
235
|
+
for (const [command, entry] of Object.entries(normalizeManagedIds(managedIds))) {
|
|
236
|
+
if (desiredNames.has(command)) continue
|
|
237
|
+
if (!entry.ours) {
|
|
238
|
+
// 非本插件创建(同名冲突/手工创建):保留 id 以便后续更新,但绝不删除
|
|
239
|
+
ids[command] = entry
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
const res = await callApi(client, token, { method: 'DELETE', path: `${SLASH_COMMAND_API_PATH}/${entry.id}` })
|
|
243
|
+
if (isBizError(res) && !isNotFound(res)) {
|
|
244
|
+
result.ids = ids
|
|
245
|
+
result.error = errorInfo(res, command)
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
delete ids[command]
|
|
249
|
+
result.deleted.push(command)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
result.ids = ids
|
|
253
|
+
result.ok = true
|
|
254
|
+
return result
|
|
255
|
+
}
|