@gavinc-cn/touchstone-dsh 0.1.1 → 0.3.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/README.md +39 -11
- package/README.zh.md +25 -10
- package/backend/lib/access.js +44 -0
- package/backend/lib/agents.js +341 -0
- package/backend/lib/args.js +94 -0
- package/backend/lib/auth.js +195 -0
- package/backend/lib/board.js +6154 -0
- package/backend/lib/builtin-assets.js +1177 -0
- package/backend/lib/casescan.js +456 -0
- package/backend/lib/chat.js +2053 -0
- package/backend/lib/cookie.js +76 -0
- package/backend/lib/db.js +2260 -0
- package/backend/lib/dshdriver.js +1148 -0
- package/backend/lib/dshevents.js +851 -0
- package/backend/lib/export-cases.js +261 -0
- package/backend/lib/feishu.js +1738 -0
- package/backend/lib/feishu_cfg.js +228 -0
- package/backend/lib/feishu_conv.js +1484 -0
- package/backend/lib/feishu_device.js +428 -0
- package/backend/lib/feishu_inbound.js +1674 -0
- package/backend/lib/feishu_provision.js +855 -0
- package/backend/lib/feishu_rest.js +398 -0
- package/backend/lib/feishu_slash.js +568 -0
- package/backend/lib/feishu_ws.js +950 -0
- package/backend/lib/http.js +312 -0
- package/backend/lib/lib.js +973 -0
- package/backend/lib/loadcase.js +749 -0
- package/backend/lib/loadgen.js +1407 -0
- package/backend/lib/localbus.js +164 -0
- package/backend/lib/lock.js +405 -0
- package/backend/lib/proc.js +278 -0
- package/backend/lib/prompts.js +1021 -0
- package/backend/lib/pybase64.js +114 -0
- package/backend/lib/pyjson.js +214 -0
- package/backend/lib/query.js +125 -0
- package/backend/lib/rag.js +1310 -0
- package/backend/lib/runner.js +4520 -0
- package/backend/lib/servercaps.js +182 -0
- package/backend/lib/serverstate.js +237 -0
- package/backend/lib/sessparse.js +1176 -0
- package/backend/lib/sse.js +207 -0
- package/backend/lib/static.js +84 -0
- package/backend/lib/taskjson.js +302 -0
- package/backend/lib/waitq.js +2114 -0
- package/backend/lib/worktree.js +588 -0
- package/backend/lib/zstd-frames.js +218 -0
- package/backend/routes/admin.js +553 -0
- package/backend/routes/agents.js +149 -0
- package/backend/routes/assets.js +145 -0
- package/backend/routes/board.js +1220 -0
- package/backend/routes/feishu.js +269 -0
- package/backend/routes/feishu_app.js +215 -0
- package/backend/routes/feishu_hook.js +358 -0
- package/backend/routes/index.js +297 -0
- package/backend/routes/projects.js +1059 -0
- package/backend/routes/sessions.js +946 -0
- package/backend/routes/tasks.js +1530 -0
- package/backend/schema/fixtures/ancient.sql +189 -0
- package/backend/schema/fixtures/pre_alter.sql +177 -0
- package/backend/schema/migrations.json +154 -0
- package/backend/schema/schema.json +1451 -0
- package/backend/schema/schema.sql +216 -0
- package/backend/server.js +498 -0
- package/board.py +805 -72
- package/chat.py +590 -17
- package/db.py +356 -140
- package/dsh-plugin/cordis.patch.yml +7 -3
- package/dsh-plugin/install.sh +28 -11
- package/dsh-plugin/lib/agent-driver.js +710 -89
- package/dsh-plugin/lib/client.bundle.js +94 -9
- package/dsh-plugin/lib/index.js +529 -39
- package/dsh-plugin/lib/pack-closure.mjs +291 -0
- package/dsh-plugin/scripts/prepack-check.mjs +30 -2
- package/dsh-plugin/src/client.js +94 -9
- package/dshdriver.py +74 -5
- package/dshevents.py +65 -3
- package/feishu.py +1197 -34
- package/feishu_conv.py +920 -0
- package/lifecycle.py +116 -13
- package/package.json +6 -1
- package/requirements.txt +2 -1
- package/server.py +218 -26
- package/sessparse.py +150 -44
- package/touchstone.py +6 -3
- package/waitq.py +23 -0
- package/webui/dist-plugin/assets/index-ChP3sg6y.css +1 -0
- package/webui/dist-plugin/assets/index-UrMyeAXF.js +484 -0
- package/webui/dist-plugin/index.html +2 -2
- package/webui/dist-plugin/assets/index-B0WoP1Qy.css +0 -1
- package/webui/dist-plugin/assets/index-DtwKoi1T.js +0 -473
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由族:个人飞书配置 / 绑定 / 投递记录 —— 7 个端点(M7-R3)。
|
|
3
|
+
*
|
|
4
|
+
* 归属条目(顺序 = 匹配顺序,见 `./index.js` 文件头):
|
|
5
|
+
* - `GET /api/me/feishu-cfg` → `_api_me_feishu_cfg_get`(server.py:1734)
|
|
6
|
+
* - `POST /api/me/feishu-cfg` → `_api_me_feishu_cfg_set`(server.py:1757)
|
|
7
|
+
* - `PATCH /api/me/feishu-cfg` → 同函数(server.py:1252 的 PATCH 分支)
|
|
8
|
+
* - `POST /api/me/feishu-bindcode` → `_api_me_feishu_bindcode`(server.py:2115)
|
|
9
|
+
* - `GET /api/me/feishu` → `_api_me_feishu_get`(server.py:2122)
|
|
10
|
+
* - `DELETE /api/me/feishu` → `_api_me_feishu_delete`(server.py:2136)
|
|
11
|
+
* - `GET /api/me/feishu/outbox` → `_api_me_feishu_outbox`(server.py:1808)
|
|
12
|
+
*
|
|
13
|
+
* 七条都是**字面路径**(无正则),与既有十族的前缀互不吞并:`/api/me/feishu` 与
|
|
14
|
+
* `/api/me/feishu/outbox` 各自精确相等才命中,`/api/me/feishu-cfg`、`/api/me/feishu-bindcode`
|
|
15
|
+
* 是另外两个字面量(对端车道的 `/api/me/feishu-hook/apply`、`/api/me/feishu/slash-commands`
|
|
16
|
+
* 等同样如此)⇒ 挂表位置不影响分派,本族整族追加在表末。
|
|
17
|
+
*
|
|
18
|
+
* ══ 七条「照抄才不失真」的口径(全部是 oracle 逐字实测,见计划 §1.8)═════════════════
|
|
19
|
+
* 1. **成功体两种字节形态**:`DELETE /api/me/feishu` 回的是硬编码字节串 `b'{"ok":true}'`
|
|
20
|
+
* (2141) ⇒ **紧凑、无空格**,必须 `ctx.text(200, '{"ok":true}', 'application/json; charset=utf-8')`;
|
|
21
|
+
* 同一族的 `GET|POST|PATCH /api/me/feishu-cfg` 走 `json.dumps` ⇒ `{"ok": true, …}` **带空格**,
|
|
22
|
+
* 走 `ctx.json`。实测两次:`'{"ok":true}'`(CL=11) vs `'{"ok": true, "inbound_started": false, "verify": null}'`。
|
|
23
|
+
* 2. **`GET cfg` 的八个键与固定插入序**:`enabled`/`push_enabled`/`default_webhook`/
|
|
24
|
+
* `has_default_secret`/`base_url`/`app_id`/`has_app_secret`/`inbound`(`inbound` 内
|
|
25
|
+
* `configured`/`thread`)。`json.dumps` 保持插入序 ⇒ 键序变了就是字节差。
|
|
26
|
+
* 3. **两个开关缺键按 `true` 回显**(1740/1742 的 `cfg.get(k, True)`,走 `pyTruthy`)——
|
|
27
|
+
* 存量配置零迁移;**键存在但值为假**(`null`/`0`/`""`/`[]`/`{}`)才回 false。
|
|
28
|
+
* 非空串一律真(`"0"`/`"false"` ⇒ true)。
|
|
29
|
+
* 4. **`has_*` 是 `bool(原值)`**(1743/1749):`" "` ⇒ true(**不 strip**)、`""`/`null`/`0` ⇒ false;
|
|
30
|
+
* secret 永不回显,只回布尔。`base_url`/`app_id` **不打码**。
|
|
31
|
+
* 5. **打码一律「码点」不是 UTF-16 单元**:`default_webhook` 走 `lib.js:maskWebhook`
|
|
32
|
+
* (`len<=48` 原样、否则前 48 码点 + `…`)、`open_id` 走 `lib.js:maskOpenId`
|
|
33
|
+
* (`len>6` ⇒ 前 6 码点 + `…`)。用 `String.length` 会在 emoji 边界差 1。
|
|
34
|
+
* 6. **`GET /api/me/feishu` 的 `default_project` 无归属校验**(2131-2133):拿绑定行的
|
|
35
|
+
* `default_project_id` 直查 `db.get_project`,**不查 user_id** ⇒ 指向他人项目也回显其项目名。
|
|
36
|
+
* 这是既有可观测契约,**照抄、不「顺手修」**(多用户隔离红线在「能不能改」上,不在这条只读回显上)。
|
|
37
|
+
* 7. **`outbox`**:字段恒序 `id,status,retries,last_error,created_at,target`;
|
|
38
|
+
* `db.feishu_outbox_recent(50, user_id=…)` 的 `ORDER BY id DESC LIMIT 50` **硬编码**
|
|
39
|
+
* (handler 压根没读 `parsed` ⇒ `?limit=1` 静默忽略);`user_id=0` 的遗留行对任何用户不可见
|
|
40
|
+
* (SQL 的 `WHERE user_id=?` 天然排除)。
|
|
41
|
+
*
|
|
42
|
+
* ══ 三条「看着能简化、其实一改就分叉」的坑 ═══════════════════════════════════════
|
|
43
|
+
* ① **强制改密闸只对 POST 存在**:`server.py:1023`(do_GET) 与 `:1040`(do_POST) 调
|
|
44
|
+
* `_must_change_pw_blocked`,而 `do_PATCH`(1227) / `do_DELETE`(1266) **只有 401 检查**。
|
|
45
|
+
* 实测:持一次性口令的账号 GET/POST cfg ⇒ 403 `{"error":"请先修改初始口令"}`,
|
|
46
|
+
* **PATCH cfg 与 DELETE feishu ⇒ 200**。这道闸在 `server.js`(按方法判,见那里的注释),
|
|
47
|
+
* 本文件**不许**再写一遍,也不许给 PATCH/DELETE 补门(补了就是分叉)。
|
|
48
|
+
* ② **读体的时机**:`do_POST:1048` 给每个 POST 统一读体、`do_PATCH:1262` 在调用 handler
|
|
49
|
+
* 之前读体 ⇒ 两个写方法都必须在 handler 里 `await ctx.readBody()`;`GET`/`DELETE`
|
|
50
|
+
* 全程不读体(`_api_me_feishu_delete`/`_api_me_feishu_get` 里没有 `_read_json`)。
|
|
51
|
+
* 读体还会把非对象 JSON 折成 `{}`(`_read_json` 的 `isinstance(data, dict)` 收口)⇒
|
|
52
|
+
* **handler 拿到的 body 一定是普通对象**,`Object.hasOwn(body, k)` 与 Python `k in body` 等价。
|
|
53
|
+
* ③ **非对象的 cfg 行 ⇒ 掐断连接**:`feishu_user_cfgs.json` 是合法 JSON 但非对象(`null`/`[]`/
|
|
54
|
+
* `123`)时,oracle 在 `cfg.get(...)` 上抛 AttributeError ⇒ **0 字节断连**(不是 500、不是默认值)。
|
|
55
|
+
* 故本文件的 `cfgGet` 显式拒绝非普通对象并抛 TypeError,由 `dispatchSafely` 收口成
|
|
56
|
+
* `res.destroy()`(与 `routes/feishu_hook.js` 的 `eventsAllowed` 同一约定)。
|
|
57
|
+
* 注意「坏 JSON / 空串」**不**走这条路:`db.get_feishu_user_cfg` 的 ValueError 分支给 `{}`
|
|
58
|
+
* ⇒ 安全默认(两档区别是「合法 JSON 非对象」vs「根本不是 JSON」)。
|
|
59
|
+
*
|
|
60
|
+
* ══ 与 Python 的一处**有意**差异(登记,不擅自「修」oracle)═══════════════════════
|
|
61
|
+
* `_mask_webhook`(1727) 对**非字符串**的存量值(手工改库才会出现)会 `len(123)` 抛 TypeError
|
|
62
|
+
* ⇒ 断连;本文件的 `lib.js:maskWebhook` 对非字符串走 `String(url)`(回 `"123"`)。与
|
|
63
|
+
* `routes/feishu_hook.js` 复用同一个冻结助手、保持全站单一口径,故不在这里另造一套。
|
|
64
|
+
* 只有手工改库能触发,不在 HTTP 可达面内。
|
|
65
|
+
*/
|
|
66
|
+
import {
|
|
67
|
+
delFeishuBindingByUser, feishuOutboxRecent, getFeishuBindingByUser, getProject,
|
|
68
|
+
} from '../lib/db.js';
|
|
69
|
+
import { inboundStatus, userConfig } from '../lib/feishu_cfg.js';
|
|
70
|
+
import { makeBindCode, saveUserConfig } from '../lib/feishu.js';
|
|
71
|
+
import { maskOpenId, maskWebhook } from '../lib/lib.js';
|
|
72
|
+
|
|
73
|
+
// ---------- Python 语义助手(本文件私有) ----------
|
|
74
|
+
|
|
75
|
+
/** Python 真值:`""`/`0`/`None`/`False`/空容器为假(JS 的 `[]`/`{}` 是 truthy,必须显式收口) */
|
|
76
|
+
function pyTruthy(value) {
|
|
77
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
78
|
+
if (value !== null && typeof value === 'object') return Object.keys(value).length > 0;
|
|
79
|
+
return Boolean(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `cfg.get(key, default)` —— 但**先要求 `cfg` 是普通对象**(见文件头坑③)。
|
|
84
|
+
*
|
|
85
|
+
* 不能直接复用 `feishu_cfg.js:dictGet`:那份对 `[]`/`123` 是宽(`Object.hasOwn([], k)` 回 false
|
|
86
|
+
* ⇒ 给默认值),而 oracle 在**任何一种非对象**上都抛(`[]` ⇒ `AttributeError: 'list' object
|
|
87
|
+
* has no attribute 'get'`)⇒ 0 字节断连。这里同形抛,由 `dispatchSafely` 收口。
|
|
88
|
+
*/
|
|
89
|
+
function cfgGet(cfg, key, dflt = undefined) {
|
|
90
|
+
if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
91
|
+
const name = cfg === null ? 'NoneType' : Array.isArray(cfg) ? 'list' : typeof cfg;
|
|
92
|
+
throw new TypeError(`'${name}' object has no attribute 'get'`);
|
|
93
|
+
}
|
|
94
|
+
return Object.hasOwn(cfg, key) ? cfg[key] : dflt;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --------------------------------------------------------------------------
|
|
98
|
+
// GET /api/me/feishu-cfg —— 八键 + 打码 + 入站状态
|
|
99
|
+
// --------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 当前用户飞书配置读取(`_api_me_feishu_cfg_get` 1734;所有登录用户可访问,各看各的)。
|
|
103
|
+
*
|
|
104
|
+
* 键序与口径见文件头第 2–5 条。`secret` 类只回布尔;`inbound` 是本人入站长连接状态
|
|
105
|
+
* (`feishu_cfg.js:inboundStatus`:`configured` = 凭据齐全、`thread` = 注册表里的线程还活着;
|
|
106
|
+
* M7 无注册方 ⇒ `thread` 恒 false,那是**当前线程模型的正确返回**,不是占位假值)。
|
|
107
|
+
* **全程不读体**(oracle 这个函数的正文里没有 `_read_json`)。
|
|
108
|
+
*/
|
|
109
|
+
function apiMeFeishuCfgGet(ctx) {
|
|
110
|
+
const cfg = userConfig(ctx.user.id, { db: ctx.db });
|
|
111
|
+
ctx.json(200, {
|
|
112
|
+
enabled: pyTruthy(cfgGet(cfg, 'enabled', true)),
|
|
113
|
+
// 缺键按开回显,与 `feishu.notify_events` 的判定口径一致(存量配置零迁移)
|
|
114
|
+
push_enabled: pyTruthy(cfgGet(cfg, 'push_enabled', true)),
|
|
115
|
+
default_webhook: maskWebhook(cfgGet(cfg, 'default_webhook', '')),
|
|
116
|
+
has_default_secret: pyTruthy(cfgGet(cfg, 'default_secret')),
|
|
117
|
+
base_url: cfgGet(cfg, 'base_url', ''),
|
|
118
|
+
// 入站:自建应用凭据(app_id 非密可回显;secret 只回布尔)
|
|
119
|
+
app_id: cfgGet(cfg, 'app_id', ''),
|
|
120
|
+
has_app_secret: pyTruthy(cfgGet(cfg, 'app_secret')),
|
|
121
|
+
// 本用户入站长连接状态(明示配置/连接进度,避免「存没存上」不可见)
|
|
122
|
+
inbound: inboundStatus(ctx.user.id, { db: ctx.db }),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --------------------------------------------------------------------------
|
|
127
|
+
// POST / PATCH /api/me/feishu-cfg —— 同一处理函数
|
|
128
|
+
// --------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 当前用户飞书配置更新(`_api_me_feishu_cfg_set` 1757;PATCH 走 `server.py:1252` 的同一分支)。
|
|
132
|
+
*
|
|
133
|
+
* 业务全在 `feishu.saveUserConfig`(缺省=不改、显式空串=清除、`str(v or "").strip()` 落库、
|
|
134
|
+
* 两个开关走 Python `bool()`)。响应 = `{"ok": true, **out}`,键序 `ok, inbound_started, verify`。
|
|
135
|
+
* `verify` 是凭据**两件齐全**时的即时快检结果,否则 `null`(零网络请求)。
|
|
136
|
+
*
|
|
137
|
+
* POST 的强制改密闸在 `server.js`(`server.py:1040`);PATCH **没有**这道闸
|
|
138
|
+
* (`server.py:1227` 的 `do_PATCH` 只有 401 检查)——见文件头坑①,本文件不补门。
|
|
139
|
+
*/
|
|
140
|
+
async function apiMeFeishuCfgSet(ctx) {
|
|
141
|
+
// 读体时机照 `do_POST:1048` / `do_PATCH:1262`(都在调用 handler 之前)
|
|
142
|
+
const body = await ctx.readBody();
|
|
143
|
+
const out = await saveUserConfig(ctx.user.id, body, { db: ctx.db });
|
|
144
|
+
ctx.json(200, { ok: true, ...out });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --------------------------------------------------------------------------
|
|
148
|
+
// POST /api/me/feishu-bindcode
|
|
149
|
+
// --------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 生成飞书绑定码(`_api_me_feishu_bindcode` 2115):6 位、10 分钟有效、**进程内存态**。
|
|
153
|
+
* 响应只有 `{"code": "<6 位>"}` 一个键(**无 TTL 回显**);每次调用换新码、旧码不从注册表移除。
|
|
154
|
+
* 不读体(oracle 里没有 `_read_json`)。
|
|
155
|
+
*/
|
|
156
|
+
function apiMeFeishuBindcode(ctx) {
|
|
157
|
+
ctx.json(200, { code: makeBindCode(ctx.user.id) });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --------------------------------------------------------------------------
|
|
161
|
+
// GET / DELETE /api/me/feishu
|
|
162
|
+
// --------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 当前用户绑定状态(`_api_me_feishu_get` 2122):`{"bound": bool}`,已绑才补
|
|
166
|
+
* `open_id`(**码点**打码)与 `default_project`(项目名;`default_project_id` 为 0/取不到行 ⇒ `""`)。
|
|
167
|
+
*
|
|
168
|
+
* `default_project` **无归属校验**(2131-2133 直接用绑定行里的 id 查项目,不比对 user_id)——
|
|
169
|
+
* 指向他人项目也照实回显其项目名。这是既有可观测契约,照抄(见文件头第 6 条)。
|
|
170
|
+
*/
|
|
171
|
+
function apiMeFeishuGet(ctx) {
|
|
172
|
+
const row = getFeishuBindingByUser(ctx.user.id, { db: ctx.db });
|
|
173
|
+
const bound = row !== null && row !== undefined;
|
|
174
|
+
const out = { bound };
|
|
175
|
+
if (bound) {
|
|
176
|
+
out.open_id = maskOpenId(row.open_id);
|
|
177
|
+
const proj = pyTruthy(row.default_project_id)
|
|
178
|
+
? getProject(row.default_project_id, { db: ctx.db })
|
|
179
|
+
: null;
|
|
180
|
+
out.default_project = proj ? proj.name : '';
|
|
181
|
+
}
|
|
182
|
+
ctx.json(200, out);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 当前用户解绑飞书号(`_api_me_feishu_delete` 2136)。
|
|
187
|
+
*
|
|
188
|
+
* 成功体是**硬编码字节串** `b'{"ok":true}'`(2141)——紧凑、**无空格**,与同族 cfg 的
|
|
189
|
+
* `json.dumps` 口径(`{"ok": true}`)差 1 个字节 ⇒ 必须走 `ctx.text` 而**不是** `ctx.json`。
|
|
190
|
+
* 幂等:无绑定行时 `del_feishu_binding_by_user` 删 0 行、照回同一个字节串。不读体
|
|
191
|
+
* (`do_DELETE` 里没有 `_read_json`;Python 的 `do_DELETE` 只在最前做一次 401 检查)。
|
|
192
|
+
*/
|
|
193
|
+
function apiMeFeishuDelete(ctx) {
|
|
194
|
+
delFeishuBindingByUser(ctx.user.id, { db: ctx.db });
|
|
195
|
+
ctx.text(200, '{"ok":true}', 'application/json; charset=utf-8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --------------------------------------------------------------------------
|
|
199
|
+
// GET /api/me/feishu/outbox
|
|
200
|
+
// --------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 当前用户最近投递记录(`_api_me_feishu_outbox` 1808):**数组**(不是 `{ok:…}` 包装)。
|
|
204
|
+
*
|
|
205
|
+
* 三处照抄(见文件头第 7 条):① 字段恒序 `id,status,retries,last_error,created_at,target`;
|
|
206
|
+
* ② `LIMIT 50` 与 `ORDER BY id DESC` **硬编码在 db 层**(handler 不读 query ⇒ `?limit=1` 被静默忽略);
|
|
207
|
+
* ③ `target` 走 `maskWebhook`(URL 本身含 token 即凭据)。`user_id=0` 的遗留行被 SQL 的
|
|
208
|
+
* `WHERE user_id=?` 天然挡掉——对**任何**用户都不可见。不读体。
|
|
209
|
+
*/
|
|
210
|
+
function apiMeFeishuOutbox(ctx) {
|
|
211
|
+
const rows = feishuOutboxRecent(50, ctx.user.id, { db: ctx.db });
|
|
212
|
+
ctx.json(200, rows.map((r) => ({
|
|
213
|
+
id: r.id,
|
|
214
|
+
status: r.status,
|
|
215
|
+
retries: r.retries,
|
|
216
|
+
last_error: r.last_error,
|
|
217
|
+
created_at: r.created_at,
|
|
218
|
+
target: maskWebhook(r.target),
|
|
219
|
+
})));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --------------------------------------------------------------------------
|
|
223
|
+
// 路由表(七条字面路径,彼此不吞并;整族追加在表末,见文件头)
|
|
224
|
+
// --------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
export const routes = [
|
|
227
|
+
{
|
|
228
|
+
method: 'GET',
|
|
229
|
+
path: '/api/me/feishu-cfg',
|
|
230
|
+
/** 读配置:八键定序、secret 只回布尔、webhook 打码(码点)、带本人入站状态。不读体。 */
|
|
231
|
+
handler: (ctx) => apiMeFeishuCfgGet(ctx),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
method: 'POST',
|
|
235
|
+
path: '/api/me/feishu-cfg',
|
|
236
|
+
/** 写配置(过强制改密闸,闸在 server.js):缺省=不改、空串=清除;成功体带空格。 */
|
|
237
|
+
handler: (ctx) => apiMeFeishuCfgSet(ctx),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
method: 'PATCH',
|
|
241
|
+
path: '/api/me/feishu-cfg',
|
|
242
|
+
/** 与 POST **同一处理函数**(`server.py:1252`);唯一差异=PATCH 不过强制改密闸(闸不在本文件)。 */
|
|
243
|
+
handler: (ctx) => apiMeFeishuCfgSet(ctx),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
method: 'POST',
|
|
247
|
+
path: '/api/me/feishu-bindcode',
|
|
248
|
+
/** 生成 6 位绑定码(进程内存态、10 分钟):响应只有 code,无 TTL 回显。 */
|
|
249
|
+
handler: (ctx) => apiMeFeishuBindcode(ctx),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
method: 'GET',
|
|
253
|
+
path: '/api/me/feishu',
|
|
254
|
+
/** 绑定状态:未绑只回 `{bound:false}`;已绑补打码 open_id 与项目名(**不校验归属**)。 */
|
|
255
|
+
handler: (ctx) => apiMeFeishuGet(ctx),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
method: 'DELETE',
|
|
259
|
+
path: '/api/me/feishu',
|
|
260
|
+
/** 解绑:成功体是硬编码 `{"ok":true}`(无空格),幂等。不读体。 */
|
|
261
|
+
handler: (ctx) => apiMeFeishuDelete(ctx),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
method: 'GET',
|
|
265
|
+
path: '/api/me/feishu/outbox',
|
|
266
|
+
/** 最近投递记录:数组、字段恒序、`ORDER BY id DESC LIMIT 50` 硬编码、target 打码。 */
|
|
267
|
+
handler: (ctx) => apiMeFeishuOutbox(ctx),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由族:飞书应用面(斜杠指令 + 扫码供给 + 配置自检)——M7-R4 尾/R5 车道,5 个端点。
|
|
3
|
+
*
|
|
4
|
+
* 归属条目(`server.py` 行号 = 归属条目;顺序照 `_route_get`(1318/1320) 与 `do_POST`(1062-1068)):
|
|
5
|
+
* - `GET /api/me/feishu/slash-commands` → `_api_me_feishu_slash_get`(1818) → `feishu.slash_status`
|
|
6
|
+
* - `POST /api/me/feishu/slash-commands` → `_api_me_feishu_slash_set`(1826) → sync/clear/notify
|
|
7
|
+
* - `GET /api/me/feishu/provision` → `_api_me_feishu_provision_get`(1767) → `provision_status`
|
|
8
|
+
* - `POST /api/me/feishu/provision` → `_api_me_feishu_provision_set`(1774) → start/cancel/apply/publish
|
|
9
|
+
* - `POST /api/me/feishu/doctor` → `_api_me_feishu_doctor`(1801) → `doctor`
|
|
10
|
+
*
|
|
11
|
+
* 本文件只做**接线**:业务在 `../lib/feishu_slash.js`(更早车道已落地)与
|
|
12
|
+
* `../lib/feishu_provision.js`(R5 追加段)。路由层负责的判据只有四类,全部逐字照抄 oracle:
|
|
13
|
+
*
|
|
14
|
+
* ══ ① 缺 action 两族**相反**(本族最容易被写歪的一处)═══════════════════════════════
|
|
15
|
+
* - `/slash-commands`:`action = str(body.get("action") or "sync")` ⇒ **缺省 sync**(200);
|
|
16
|
+
* 假值(`0`/`false`/`null`/`""`/`[]`/`{}`)走同一条缺省;`str()` 化后不在
|
|
17
|
+
* `("sync","clear","notify")` 里 ⇒ 400(**大小写敏感**:`"Sync"` 不认;容器走 Python repr)。
|
|
18
|
+
* - `/provision`:`action = str(body.get("action") or "")` ⇒ 缺省是**空串 ⇒ 400**;
|
|
19
|
+
* 返回体是 `json.dumps({"error": ...})` ⇒ **冒号后带空格**(`ctx.errorSpaced`),
|
|
20
|
+
* 与全站硬编码紧凑错误体(`ctx.error`)**不是一个字节形态**。
|
|
21
|
+
*
|
|
22
|
+
* ══ ② 三个动作的返回体**键序**(Python dict 插入序,`json.dumps` 保持)════════════════
|
|
23
|
+
* - `notify`:`{ok, card_sent, card_error}`(字面量)。
|
|
24
|
+
* - `sync`/`clear`:业务字典 + **追加** `card_sent`/`card_error` 两键 ⇒ 正常档是
|
|
25
|
+
* `{ok, created, updated, kept, extra, failed, need_scope, error, card_sent, card_error}`
|
|
26
|
+
* (`clear` 把 created/updated/kept 换成 deleted),而**短路档**(无凭据/列表异常)只有
|
|
27
|
+
* `{ok, need_scope, error}` + 追加两键——**键集本身也是判据**。
|
|
28
|
+
* - `need_scope=True` 时**顺手补发权限引导卡片**并把两个键覆盖成发送结果(发不出去也不影响
|
|
29
|
+
* 设置页的报错展示 ⇒ 失败**不**改 `error`)。
|
|
30
|
+
*
|
|
31
|
+
* ══ ③ 读体时机 ═══════════════════════════════════════════════════════════════════════
|
|
32
|
+
* `do_POST:1048` 在**分派之前**统一读体 ⇒ 三条 POST 一律先 `await ctx.readBody()`
|
|
33
|
+
* (400 档也要读掉),而两条 GET 全程**不读**(Python `do_GET` 没有 `_read_json`)。
|
|
34
|
+
* 这不是洁癖:没读掉的 body 会留在 keep-alive 连接上污染下一个请求。
|
|
35
|
+
*
|
|
36
|
+
* ══ ④ 真值口径 ═══════════════════════════════════════════════════════════════════════
|
|
37
|
+
* `force` 与 `send_probe` 走 Python `bool()`:**非空字符串一律真**(`"0"`/`"false"` ⇒ true),
|
|
38
|
+
* `[]`/`{}`/`null`/`""`/`0` ⇒ false —— JS 的 `Boolean([])`/`Boolean({})` 都是 true ⇒ 必须走本文件
|
|
39
|
+
* 的 `pyTruthy`。`version`/`remark`/`changelog` 走 `str(v or "")`(容器是 Python repr 形态)。
|
|
40
|
+
*
|
|
41
|
+
* 未移植项与环境差异见 `../lib/feishu_provision.js` 文件头(M9 起 Device Flow **已移植**:
|
|
42
|
+
* 协议客户端在 `../lib/feishu_device.js`,`supported` 改为真能力探测;剩 `inbound_ws` 线程态);
|
|
43
|
+
* **本文件不重复实现任何业务逻辑**。
|
|
44
|
+
*/
|
|
45
|
+
import { doctor, provisionApply, provisionCancel, provisionPublish, provisionStart,
|
|
46
|
+
provisionStatus } from '../lib/feishu_provision.js';
|
|
47
|
+
import { clearSlashCommands, sendSlashPermCard, slashStatus,
|
|
48
|
+
syncSlashCommands } from '../lib/feishu_slash.js';
|
|
49
|
+
|
|
50
|
+
// ---------- Python 语义助手(本文件私有;与 `routes/feishu_hook.js` 的同名助手同口径) ----------
|
|
51
|
+
|
|
52
|
+
/** Python 真值:`""`/`0`/`None`/`False`/空容器为假(JS 的 `[]`/`{}` 是 truthy,必须显式收口) */
|
|
53
|
+
function pyTruthy(value) {
|
|
54
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
55
|
+
if (value !== null && typeof value === 'object') return Object.keys(value).length > 0;
|
|
56
|
+
return Boolean(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Python `repr(str)` 的可用子集(单引号、转义反斜杠/引号/控制字符) */
|
|
60
|
+
function pyReprString(text) {
|
|
61
|
+
let out = "'";
|
|
62
|
+
for (const ch of String(text)) {
|
|
63
|
+
if (ch === '\\') out += '\\\\';
|
|
64
|
+
else if (ch === "'") out += "\\'";
|
|
65
|
+
else if (ch === '\n') out += '\\n';
|
|
66
|
+
else if (ch === '\r') out += '\\r';
|
|
67
|
+
else if (ch === '\t') out += '\\t';
|
|
68
|
+
else if (ch.codePointAt(0) < 0x20 || ch.codePointAt(0) === 0x7f) {
|
|
69
|
+
out += `\\x${ch.codePointAt(0).toString(16).padStart(2, '0')}`;
|
|
70
|
+
} else out += ch;
|
|
71
|
+
}
|
|
72
|
+
return `${out}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Python `repr(x)` 的可用子集(str 走 repr 规则,其余用 `pyStr`) */
|
|
76
|
+
function pyRepr(value) {
|
|
77
|
+
if (typeof value === 'string') return pyReprString(value);
|
|
78
|
+
return pyStr(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Python `str(v or "")`:str 原样;`None`→`"None"`、bool→`"True"`/`"False"`、数字→十进制文本、
|
|
83
|
+
* **容器→repr 形态**(`["sync"]`⇒`"['sync']"`、`{"a":1}`⇒`"{'a': 1}"`)。
|
|
84
|
+
*
|
|
85
|
+
* 本文件的用途只有一处:把 `action` 折成**与 oracle 同一把尺子**的字符串再去白名单比。
|
|
86
|
+
* 用 `JSON.stringify` 兜底会给出 `["sync"]`——同样不在白名单(结果同为 400),但同一个 `str()`
|
|
87
|
+
* 口径还会经 `version`/`remark`/`changelog` 真的漏进请求体,故照 Python repr 写全。
|
|
88
|
+
*/
|
|
89
|
+
function pyStr(value) {
|
|
90
|
+
if (typeof value === 'string') return value;
|
|
91
|
+
if (value === null || value === undefined) return 'None';
|
|
92
|
+
if (typeof value === 'boolean') return value ? 'True' : 'False';
|
|
93
|
+
if (typeof value === 'number') return String(value);
|
|
94
|
+
if (Array.isArray(value)) return `[${value.map(pyRepr).join(', ')}]`;
|
|
95
|
+
if (typeof value === 'object') {
|
|
96
|
+
const items = Object.keys(value).map((k) => `${pyRepr(k)}: ${pyRepr(value[k])}`);
|
|
97
|
+
return `{${items.join(', ')}}`;
|
|
98
|
+
}
|
|
99
|
+
return String(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --------------------------------------------------------------------------
|
|
103
|
+
// 路由实现
|
|
104
|
+
// --------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/** `_api_me_feishu_slash_get`(1818):现状只读(缺凭据/缺权限都走 `error` 字段,不报错) */
|
|
107
|
+
async function apiSlashGet(ctx) {
|
|
108
|
+
ctx.json(200, await slashStatus(ctx.user.id, { db: ctx.db }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `_api_me_feishu_slash_set`(1826):三动作;缺省 sync;缺 scope 时自动补发引导卡片 */
|
|
112
|
+
async function apiSlashSet(ctx) {
|
|
113
|
+
const body = await ctx.readBody(); // 读体在分派之前(见文件头③)
|
|
114
|
+
const action = pyStr(pyTruthy(body.action) ? body.action : 'sync');
|
|
115
|
+
if (action !== 'sync' && action !== 'clear' && action !== 'notify') {
|
|
116
|
+
ctx.errorSpaced(400, 'action 仅支持 sync / clear / notify');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const options = { db: ctx.db };
|
|
120
|
+
let out;
|
|
121
|
+
if (action === 'notify') {
|
|
122
|
+
const [ok, err] = await sendSlashPermCard(ctx.user.id, options);
|
|
123
|
+
out = { ok, card_sent: ok, card_error: err };
|
|
124
|
+
} else {
|
|
125
|
+
out = action === 'sync'
|
|
126
|
+
? await syncSlashCommands(ctx.user.id, options)
|
|
127
|
+
: await clearSlashCommands(ctx.user.id, options);
|
|
128
|
+
// 追加两键(Python 的元组赋值把**不存在的键**加在 dict 末尾;已存在的键改值不改位)
|
|
129
|
+
out.card_sent = false;
|
|
130
|
+
out.card_error = '';
|
|
131
|
+
if (pyTruthy(out.need_scope)) {
|
|
132
|
+
// 缺权限:顺手把引导卡片推到飞书(发不出去也不影响设置页的报错展示)
|
|
133
|
+
const [ok, err] = await sendSlashPermCard(ctx.user.id, options);
|
|
134
|
+
out.card_sent = ok;
|
|
135
|
+
out.card_error = err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
ctx.json(200, out);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `_api_me_feishu_provision_get`(1767):能力/总闸/状态/确认链接/步骤(只读) */
|
|
142
|
+
async function apiProvisionGet(ctx) {
|
|
143
|
+
ctx.json(200, provisionStatus(ctx.user.id, { db: ctx.db }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `_api_me_feishu_provision_set`(1774):四动作;缺 action ⇒ 400(与 slash 族**相反**) */
|
|
147
|
+
async function apiProvisionSet(ctx) {
|
|
148
|
+
const body = await ctx.readBody();
|
|
149
|
+
const action = pyStr(pyTruthy(body.action) ? body.action : '');
|
|
150
|
+
const uid = ctx.user.id;
|
|
151
|
+
const options = { db: ctx.db };
|
|
152
|
+
let out;
|
|
153
|
+
if (action === 'start') {
|
|
154
|
+
out = await provisionStart(uid, pyTruthy(body.force), options);
|
|
155
|
+
} else if (action === 'cancel') {
|
|
156
|
+
out = provisionCancel(uid);
|
|
157
|
+
} else if (action === 'apply') {
|
|
158
|
+
out = await provisionApply(uid, options);
|
|
159
|
+
} else if (action === 'publish') {
|
|
160
|
+
out = await provisionPublish(uid, {
|
|
161
|
+
// `str(body.get("version") or "") or None`:空串 ⇒ `None` ⇒ publish 内部自动递增版本
|
|
162
|
+
version: pyStr(pyTruthy(body.version) ? body.version : ''),
|
|
163
|
+
remark: pyStr(pyTruthy(body.remark) ? body.remark : ''),
|
|
164
|
+
changelog: pyStr(pyTruthy(body.changelog) ? body.changelog : ''),
|
|
165
|
+
db: ctx.db,
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
ctx.errorSpaced(400, 'action 仅支持 start / cancel / apply / publish');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
ctx.json(200, out);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `_api_me_feishu_doctor`(1801):八项自检;`send_probe` 走 Python `bool()` */
|
|
175
|
+
async function apiDoctor(ctx) {
|
|
176
|
+
const body = await ctx.readBody();
|
|
177
|
+
ctx.json(200, await doctor(ctx.user.id, pyTruthy(body.send_probe), { db: ctx.db }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --------------------------------------------------------------------------
|
|
181
|
+
// 路由表(5 条;前缀 `/api/me/feishu*` 与其它族互不重叠,挂表位置不影响分派,见 `./index.js`)
|
|
182
|
+
// --------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
export const routes = [
|
|
185
|
+
{
|
|
186
|
+
method: 'GET',
|
|
187
|
+
path: '/api/me/feishu/slash-commands',
|
|
188
|
+
/** 斜杠指令现状(期望清单 17 条 + 远端现状 + 引导文案)。 */
|
|
189
|
+
handler: (ctx) => apiSlashGet(ctx),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
method: 'POST',
|
|
193
|
+
path: '/api/me/feishu/slash-commands',
|
|
194
|
+
/** sync / clear / notify;缺 action 缺省 **sync**;非法 ⇒ 400(带空格体)。 */
|
|
195
|
+
handler: (ctx) => apiSlashSet(ctx),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
method: 'GET',
|
|
199
|
+
path: '/api/me/feishu/provision',
|
|
200
|
+
/** 扫码供给状态(只读;`supported` 是**运行时能力探测**,M9 起 Node 侧真判定,见 lib 文件头)。 */
|
|
201
|
+
handler: (ctx) => apiProvisionGet(ctx),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
method: 'POST',
|
|
205
|
+
path: '/api/me/feishu/provision',
|
|
206
|
+
/** start / cancel / apply / publish;缺 action ⇒ **400**(与 slash 族相反)。 */
|
|
207
|
+
handler: (ctx) => apiProvisionSet(ctx),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
method: 'POST',
|
|
211
|
+
path: '/api/me/feishu/doctor',
|
|
212
|
+
/** 配置自检八项(`send_probe=true` 时额外发一条测试消息,60s 频控)。 */
|
|
213
|
+
handler: (ctx) => apiDoctor(ctx),
|
|
214
|
+
},
|
|
215
|
+
];
|