@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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由族:智能体探测三条(M6-6)
|
|
3
|
+
*
|
|
4
|
+
* GET /api/agents/scan → `server.py:1330`(`agents.scan_agents()`)
|
|
5
|
+
* GET /api/agents/models → `server.py:1333`(`agent_models(agent_path)`)
|
|
6
|
+
* GET /api/agents/skills → `server.py:1338`(`{"skills": scan_skills(agent_path, project_dir)}`)
|
|
7
|
+
*
|
|
8
|
+
* 三条都是**登录即可读**(`do_GET` 顶部只查 `_current_user()`,不吃 `_require_admin`),
|
|
9
|
+
* 成功体一律 `json.dumps(..., ensure_ascii=False)` ⇒ `ctx.json`;本族**没有**任何写口,
|
|
10
|
+
* 也没有错误体(参数缺省各自回落空串)。
|
|
11
|
+
*
|
|
12
|
+
* `agent_models` / `_dsh_plugin_models`(server.py:147/208)本该住 `lib/`,但 M6-6 的
|
|
13
|
+
* 冻结契约不许改 `backend/lib/**`;它们只被这一条端点消费,故落在本文件(**唯一**消费者),
|
|
14
|
+
* 语义逐行照 oracle:
|
|
15
|
+
* - 值/显示分离(`name` = `provider/<id>` 供宿主 `selectModel` 严格匹配,
|
|
16
|
+
* `display_name` = `provider/<显示名>` 供下拉展示);
|
|
17
|
+
* - 空 `agent_path`(db 默认 '')与未知路径都归默认族 ⇒ **不列模型**、只回读部署默认模型;
|
|
18
|
+
* - 结果进 60s 的 `_MODELS_CACHE`(键 = agent_path),驱动不可用时回空表且**不缓存**。
|
|
19
|
+
*/
|
|
20
|
+
import * as agents from '../lib/agents.js';
|
|
21
|
+
import * as dshdriver from '../lib/dshdriver.js';
|
|
22
|
+
import { dshDefaultModel } from '../lib/servercaps.js';
|
|
23
|
+
|
|
24
|
+
/** `_MODELS_CACHE_TTL`(server.py:139):60s */
|
|
25
|
+
const MODELS_CACHE_TTL = 60.0;
|
|
26
|
+
|
|
27
|
+
/** `_MODELS_CACHE`(server.py:138):agent_path → `[时刻秒, 结果]` */
|
|
28
|
+
const MODELS_CACHE = new Map();
|
|
29
|
+
|
|
30
|
+
/** Python `dict.get(k) or ""` 后 `str(...)`:非字符串(数组/数字/null)→ 空串(`_req_str`) */
|
|
31
|
+
function reqStr(value) {
|
|
32
|
+
return typeof value === 'string' ? value : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Python `(x or {}).get(key)`:**假值先折成空 dict**(`None`/`[]`/`''`/`0` 都退回 `undefined`),
|
|
37
|
+
* 其余非 dict(非空 list / 字符串 / 数字)在 Python 抛 AttributeError(端点打成 500、
|
|
38
|
+
* 只断这一条连接)——这里抛 TypeError 交给 `dispatchSafely`,**不静默兜底**,
|
|
39
|
+
* 否则驱动返回异常形状时两侧行为分叉(一边 500/断连、一边 200)。
|
|
40
|
+
*/
|
|
41
|
+
function dictGet(value, key) {
|
|
42
|
+
if (value === null || value === undefined || value === false || value === 0
|
|
43
|
+
|| value === '' || (Array.isArray(value) && value.length === 0)) {
|
|
44
|
+
return undefined; // `x or {}`
|
|
45
|
+
}
|
|
46
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
47
|
+
throw new TypeError(`'${Array.isArray(value) ? 'list' : typeof value}'`
|
|
48
|
+
+ " object has no attribute 'get'");
|
|
49
|
+
}
|
|
50
|
+
return Object.prototype.hasOwnProperty.call(value, key) ? value[key] : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `_dsh_plugin_models`(147):dsh 插件族模型列表(值/显示分离 + 思考等级透传)。
|
|
55
|
+
* 驱动不可用(`DshDriverError`)⇒ `{"models": [], "default": ""}`,**不入缓存**。
|
|
56
|
+
*/
|
|
57
|
+
async function dshPluginModels(cacheKey) {
|
|
58
|
+
const hit = MODELS_CACHE.get(cacheKey);
|
|
59
|
+
if (hit && Date.now() / 1000 - hit[0] < MODELS_CACHE_TTL) return hit[1];
|
|
60
|
+
const models = [];
|
|
61
|
+
let def = '';
|
|
62
|
+
let catalog;
|
|
63
|
+
try {
|
|
64
|
+
catalog = await dshdriver.models();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (e instanceof dshdriver.DshDriverError) return { models: [], default: '' };
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
for (const group of dictGet(catalog, 'groups') || []) {
|
|
70
|
+
const provider = strOf(dictGet(group, 'id'));
|
|
71
|
+
for (const m of dictGet(group, 'models') || []) {
|
|
72
|
+
const mid = strOf(dictGet(m, 'id')).trim();
|
|
73
|
+
const label = strOf(dictGet(m, 'name')).trim() || mid; // 显示名缺省回落 id
|
|
74
|
+
if (!mid && !label) continue;
|
|
75
|
+
const modelId = mid || label;
|
|
76
|
+
// 思考等级选项:只认 id 落在平台白名单内的项,名字缺省回落 id
|
|
77
|
+
const efforts = [];
|
|
78
|
+
for (const e of dictGet(m, 'efforts') || []) {
|
|
79
|
+
const eid = strOf(dictGet(e, 'id')).trim();
|
|
80
|
+
if (!dshdriver.EFFORT_LEVELS.includes(eid)) continue;
|
|
81
|
+
efforts.push({ id: eid, name: strOf(dictGet(e, 'name')).trim() || eid });
|
|
82
|
+
}
|
|
83
|
+
models.push({
|
|
84
|
+
name: provider ? `${provider}/${modelId}` : modelId,
|
|
85
|
+
display_name: provider ? `${provider}/${label}` : label,
|
|
86
|
+
efforts,
|
|
87
|
+
default_effort: strOf(dictGet(m, 'default_effort')),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const sel = dictGet(catalog, 'default') || {};
|
|
92
|
+
if (dictGet(sel, 'provider') && dictGet(sel, 'model')) {
|
|
93
|
+
def = `${dictGet(sel, 'provider')}/${dictGet(sel, 'model')}`;
|
|
94
|
+
}
|
|
95
|
+
const result = { models, default: def };
|
|
96
|
+
MODELS_CACHE.set(cacheKey, [Date.now() / 1000, result]);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Python `str(x or "")`(标量面;容器按 JSON 文本兜底 —— 与全仓私有助手同口径) */
|
|
101
|
+
function strOf(value) {
|
|
102
|
+
if (value === null || value === undefined || value === false || value === 0) return '';
|
|
103
|
+
if (typeof value === 'string') return value;
|
|
104
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
105
|
+
if (Array.isArray(value)) return value.length ? JSON.stringify(value) : '';
|
|
106
|
+
if (typeof value === 'object') return Object.keys(value).length ? JSON.stringify(value) : '';
|
|
107
|
+
return String(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `agent_models`(208):指定 agent 的可用模型(空/未知路径 ⇒ 空列表 + 部署默认模型)。
|
|
112
|
+
* 单族世界(P7b):只剩 `dsh_plugin`(前缀 `dsh-plugin:`)。
|
|
113
|
+
*/
|
|
114
|
+
async function agentModels(agentPath) {
|
|
115
|
+
const ap = reqStr(agentPath).trim();
|
|
116
|
+
if (ap.startsWith(agents.DSH_PLUGIN_PREFIX)) return dshPluginModels(ap);
|
|
117
|
+
const hit = MODELS_CACHE.get(ap);
|
|
118
|
+
if (hit && Date.now() / 1000 - hit[0] < MODELS_CACHE_TTL) return hit[1];
|
|
119
|
+
const result = { models: [], default: dshDefaultModel() };
|
|
120
|
+
MODELS_CACHE.set(ap, [Date.now() / 1000, result]);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const routes = [
|
|
125
|
+
{
|
|
126
|
+
method: 'GET',
|
|
127
|
+
path: '/api/agents/scan',
|
|
128
|
+
/** `scan_agents()`:AGENT_DEFS + EXTRA_DIRS 探测(`which` + `--version`) */
|
|
129
|
+
handler: (ctx) => {
|
|
130
|
+
ctx.json(200, agents.scanAgents());
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
method: 'GET',
|
|
135
|
+
path: '/api/agents/models',
|
|
136
|
+
/** `agent_models(parsed["agent_path"][0])`:模型下拉数据源(宿主 `/models`) */
|
|
137
|
+
handler: async (ctx) => {
|
|
138
|
+
ctx.json(200, await agentModels(ctx.q('agent_path', '')));
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
method: 'GET',
|
|
143
|
+
path: '/api/agents/skills',
|
|
144
|
+
/** `scan_skills(agent_path, project_dir)`:用户级 + 项目级 SKILL.md 扫描(dsh 恒为空) */
|
|
145
|
+
handler: (ctx) => {
|
|
146
|
+
ctx.json(200, { skills: agents.scanSkills(ctx.q('agent_path', ''), ctx.q('project_dir', '')) });
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
];
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由族:平台内置资产(skill / dsh 插件)两条(M6-6)
|
|
3
|
+
*
|
|
4
|
+
* GET /api/builtin-assets → `server.py:1326` → `_api_builtin_assets_list`(1946)
|
|
5
|
+
* POST /api/builtin-assets/<id>/(install|uninstall) → `server.py:1085` → `_api_builtin_asset_action`(1971)
|
|
6
|
+
*
|
|
7
|
+
* 权限与隔离(照 oracle 的 `_assets_target`(1930),**逐条不同**):
|
|
8
|
+
* - `target=user`(缺省):读状态**登录即可**;写(install/uninstall)仅管理员 ⇒ 403
|
|
9
|
+
* `json.dumps` 形态(**带空格**)。用户级写入的是服务进程 HOME 下的 agent 配置
|
|
10
|
+
* (影响本机所有项目),故写口必须管理员。
|
|
11
|
+
* - `target=project`:读/写都要过项目归属;越权/不存在 ⇒ **紧凑** 404 `project not found`
|
|
12
|
+
* (与既有按 project_id 的端点同口径)。`project_id` 必须是 JSON **int**(bool 不算),
|
|
13
|
+
* 否则 400(`json.dumps` 形态)。
|
|
14
|
+
* - 其它 target ⇒ 400 `target 必须是 user 或 project`(`json.dumps` 形态)。
|
|
15
|
+
*
|
|
16
|
+
* 落点(`builtin-assets.js` 的 `ext_dir()` / `dsh_profiles_dir()`):资产**清单**根默认是仓库
|
|
17
|
+
* `extensions/`(`TS_EXT_DIR` 覆盖),安装产物落 `~/.dsh/skills`(`TS_DSH_PROFILES` 覆盖)
|
|
18
|
+
* —— 单测必须把这两个 env 指到夹具,**绝不允许**在真 `~/.dsh` 上做安装/卸载。
|
|
19
|
+
*/
|
|
20
|
+
import * as assets from '../lib/builtin-assets.js';
|
|
21
|
+
|
|
22
|
+
/** `_is_admin`:管理员判据只有用户名等于 `admin`(与 `routes/admin.js` 同款) */
|
|
23
|
+
function isAdmin(ctx) {
|
|
24
|
+
return Boolean(ctx.user) && ctx.user.username === 'admin';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Python `str(x or "")`(标量面;target/asset_id 这类字段都是字符串) */
|
|
28
|
+
function pyStrOrEmpty(value) {
|
|
29
|
+
if (value === null || value === undefined || value === false || value === 0) return '';
|
|
30
|
+
if (typeof value === 'string') return value;
|
|
31
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
32
|
+
if (Array.isArray(value)) return value.length ? JSON.stringify(value) : '';
|
|
33
|
+
if (typeof value === 'object') return Object.keys(value).length ? JSON.stringify(value) : '';
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `_req_str`(580):非字符串(数组/数字/null)一律按空串处理 */
|
|
38
|
+
function reqStr(value) {
|
|
39
|
+
return typeof value === 'string' ? value : '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `_assets_target`(1930):解析资产操作目标 → `{ok, project_dir, pid}`;错误路径已回响应。
|
|
44
|
+
* `write=true` 时用户级要管理员;项目级无论读写都要归属(越权 404 紧凑)。
|
|
45
|
+
*/
|
|
46
|
+
function assetsTarget(ctx, target, projectId, write = false) {
|
|
47
|
+
if (target === 'user') {
|
|
48
|
+
if (write && !isAdmin(ctx)) {
|
|
49
|
+
ctx.errorSpaced(403, '用户级安装影响本机所有项目,仅管理员可操作');
|
|
50
|
+
return { ok: false, projectDir: '', pid: null };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, projectDir: '', pid: null };
|
|
53
|
+
}
|
|
54
|
+
if (target !== 'project') {
|
|
55
|
+
ctx.errorSpaced(400, 'target 必须是 user 或 project');
|
|
56
|
+
return { ok: false, projectDir: '', pid: null };
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isInteger(projectId) || typeof projectId === 'boolean') {
|
|
59
|
+
ctx.errorSpaced(400, '项目级操作需要 project_id');
|
|
60
|
+
return { ok: false, projectDir: '', pid: null };
|
|
61
|
+
}
|
|
62
|
+
const row = ctx.ownedProject(projectId); // 已回 404 project not found(紧凑)
|
|
63
|
+
if (row === null) return { ok: false, projectDir: '', pid: null };
|
|
64
|
+
return { ok: true, projectDir: row.project_dir, pid: row.id };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `_api_builtin_assets_list`(1946):清单 + 指定目标下的安装状态。
|
|
69
|
+
* `can_install` 为当前用户在该目标的写权限;逐资产 `supported` 为「该资产是否支持此目标」。
|
|
70
|
+
*/
|
|
71
|
+
function apiBuiltinAssetsList(ctx) {
|
|
72
|
+
const target = (reqStr(ctx.q('target', 'user')) || 'user').trim();
|
|
73
|
+
const rawPid = reqStr(ctx.q('project_id', '')).trim();
|
|
74
|
+
// Python `int(raw_pid) if raw_pid.isdigit() else None`:Unicode 数字面按全仓 ASCII 口径
|
|
75
|
+
const pidIn = /^[0-9]+$/.test(rawPid) ? Number(rawPid) : null;
|
|
76
|
+
const resolved = assetsTarget(ctx, target, pidIn);
|
|
77
|
+
if (!resolved.ok) return;
|
|
78
|
+
let list;
|
|
79
|
+
try {
|
|
80
|
+
list = assets.listAssets(target, resolved.projectDir);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e instanceof assets.AssetError) {
|
|
83
|
+
ctx.errorSpaced(e.code, e.message);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
const canInstall = target === 'user' ? isAdmin(ctx) : true;
|
|
89
|
+
ctx.json(200, {
|
|
90
|
+
target,
|
|
91
|
+
project_id: resolved.pid,
|
|
92
|
+
can_install: canInstall,
|
|
93
|
+
assets: list,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `_api_builtin_asset_action`(1971):安装(收敛到期望状态)/ 卸载(删清单内文件)。
|
|
99
|
+
* 失败按 `AssetError.code` 回 400/500;磁盘异常(OSError)统一 500 带原因。
|
|
100
|
+
*/
|
|
101
|
+
async function apiBuiltinAssetAction(ctx, assetId, action) {
|
|
102
|
+
const body = await ctx.readBody(); // Python 在 do_POST:1048 统一读体
|
|
103
|
+
const target = (reqStr(body.target) || 'user').trim();
|
|
104
|
+
const rawPid = body.project_id;
|
|
105
|
+
const pid = (typeof rawPid === 'number' && Number.isInteger(rawPid) && typeof rawPid !== 'boolean')
|
|
106
|
+
? rawPid : null;
|
|
107
|
+
const resolved = assetsTarget(ctx, target, pid, true);
|
|
108
|
+
if (!resolved.ok) return;
|
|
109
|
+
const asset = assets.getAsset(assetId);
|
|
110
|
+
if (asset === null || asset === undefined) {
|
|
111
|
+
ctx.errorSpaced(404, `未知资产:${assetId}`); // json.dumps 形态(带空格)
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = action === 'install'
|
|
117
|
+
? assets.install(asset, target, resolved.projectDir)
|
|
118
|
+
: assets.uninstall(asset, target, resolved.projectDir);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e instanceof assets.AssetError) {
|
|
121
|
+
ctx.errorSpaced(e.code, e.message);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// 磁盘异常(权限/空间)统一 500,带原因供前端 toast(Python 只吞 OSError)
|
|
125
|
+
if (e && typeof e.code === 'string') {
|
|
126
|
+
ctx.errorSpaced(500, `操作失败:${pyStrOrEmpty(e.message)}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
ctx.json(200, result);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const routes = [
|
|
135
|
+
{
|
|
136
|
+
method: 'GET',
|
|
137
|
+
path: '/api/builtin-assets',
|
|
138
|
+
handler: (ctx) => apiBuiltinAssetsList(ctx),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
method: 'POST',
|
|
142
|
+
re: /^\/api\/builtin-assets\/([a-z0-9][a-z0-9._-]*)\/(install|uninstall)$/,
|
|
143
|
+
handler: (ctx, m) => apiBuiltinAssetAction(ctx, m[1], m[2]),
|
|
144
|
+
},
|
|
145
|
+
];
|