@xrn1997/dsh-session-delete 0.6.1 → 0.7.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 +18 -10
- package/THIRD_PARTY_NOTICES.md +179 -0
- package/cordis.patch.yml +1 -1
- package/docs/screenshots/screenshot-01.png +0 -0
- package/docs/screenshots/screenshot-02.png +0 -0
- package/docs/screenshots/screenshot-03.png +0 -0
- package/icon.svg +24 -18
- package/lib/client.js +1079 -245
- package/lib/index.js +206 -71
- package/lib/types/api/connection-route.d.ts +1 -1
- package/lib/types/api/dispatch.d.ts +1 -1
- package/lib/types/api/route-table.d.ts +12 -7
- package/lib/types/client/config-settings.d.ts +13 -0
- package/lib/types/client/context.d.ts +3 -0
- package/lib/types/client/copy.d.ts +34 -7
- package/lib/types/client/detail-blocks.d.ts +25 -0
- package/lib/types/client/index.d.ts +3 -2
- package/lib/types/client/maintenance-settings.d.ts +6 -7
- package/lib/types/client/remote.d.ts +32 -8
- package/lib/types/client/settings-cards.d.ts +19 -0
- package/lib/types/client/settings-view.d.ts +9 -0
- package/lib/types/client/trash-detail.d.ts +1 -1
- package/lib/types/client/trash-view.d.ts +31 -5
- package/lib/types/client/ui/icons.d.ts +23 -7
- package/lib/types/client/ui/recipe.d.ts +2 -0
- package/lib/types/client/ui/styles.d.ts +2 -0
- package/lib/types/client/use-detail-pager.d.ts +33 -0
- package/lib/types/config.d.ts +10 -11
- package/lib/types/deps.d.ts +9 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/ports/agent-handles.d.ts +10 -2
- package/lib/types/shared/wire.d.ts +37 -5
- package/lib/types/verbs/config.d.ts +25 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +6 -6
- package/package.json +20 -14
package/lib/index.js
CHANGED
|
@@ -4,14 +4,14 @@ import zlib from "node:zlib";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
//#region src/shared/wire.ts
|
|
6
6
|
/**
|
|
7
|
-
* 跨半 wire
|
|
7
|
+
* 跨半 wire 契约:前缀、十个端点、请求/响应形状与失败码。
|
|
8
8
|
* Node 半(src/api/dispatch.ts)与浏览器半(src/client/remote.ts)共用这一份,不各写字面量。
|
|
9
9
|
*
|
|
10
10
|
* 构建约束:本文件零运行时依赖、无 import;浏览器半把整个文件 inline 进 bundle。
|
|
11
11
|
* 信封 {ok:true,value} / {ok:false,error:{code,message,details?}} 也是两半的共同契约。
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* 插件专属前缀(十段相对它命名)。
|
|
15
15
|
*
|
|
16
16
|
* 为什么必须带插件名:宿主 `connection.fetch.register` 对**重复的精确路径**直接抛错
|
|
17
17
|
* (`connection: exact Fetch route "…" is already registered`),撞了就是整棵插件树 boot 失败——
|
|
@@ -19,11 +19,11 @@ import { homedir } from "node:os";
|
|
|
19
19
|
*/
|
|
20
20
|
const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
|
|
21
21
|
/**
|
|
22
|
-
* 唯一的取数基础路径:宿主 `/api` +
|
|
22
|
+
* 唯一的取数基础路径:宿主 `/api` + 插件前缀。十段 exact Fetch 路由、客户端探测与附件 URL
|
|
23
23
|
* 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
|
|
24
24
|
*/
|
|
25
25
|
const OFFICIAL_SESSION_DELETE_API_PREFIX = `/api${SESSION_DELETE_API_PREFIX}`;
|
|
26
|
-
/**
|
|
26
|
+
/** 十段段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
|
|
27
27
|
* (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
|
|
28
28
|
* 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
|
|
29
29
|
* 两半都不逐段手写一份。
|
|
@@ -38,7 +38,8 @@ const OFFICIAL_SESSION_DELETE_API_PREFIX = `/api${SESSION_DELETE_API_PREFIX}`;
|
|
|
38
38
|
* 这一段只负责"有多少、在哪、多大"。
|
|
39
39
|
* - `attachment`:附件原始字节(全仓唯一的二进制响应段;失败仍回 JSON 信封)。
|
|
40
40
|
* - `attachmentOrphans`:无引用图片对象的扫描报告(纯读;数量/字节 + 供预览的对象清单)。
|
|
41
|
-
* - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。
|
|
41
|
+
* - `attachmentClean`:清掉报告那一刻无引用的图片对象(聚合回执 `AttachmentCleanupWire`)。
|
|
42
|
+
* - `config`:三只可调项的读/写共用一段——空体读当前值,带字段写回 profile 的 patch 层。 */
|
|
42
43
|
const ROUTES = {
|
|
43
44
|
list: {
|
|
44
45
|
path: "/list",
|
|
@@ -49,6 +50,11 @@ const ROUTES = {
|
|
|
49
50
|
path: "/ping",
|
|
50
51
|
method: "GET"
|
|
51
52
|
},
|
|
53
|
+
/** 可调项:空体读(与 `/detail` 同法,用 POST 表达一个读);带字段写,写必须进到达序链。 */
|
|
54
|
+
config: {
|
|
55
|
+
path: "/config",
|
|
56
|
+
method: "POST"
|
|
57
|
+
},
|
|
52
58
|
/** 单条详情(读回收站目录里那条会话自己的日志)。 */
|
|
53
59
|
detail: {
|
|
54
60
|
path: "/detail",
|
|
@@ -85,6 +91,22 @@ const ROUTES = {
|
|
|
85
91
|
method: "POST"
|
|
86
92
|
}
|
|
87
93
|
};
|
|
94
|
+
/** 三只可调项的接受范围(闭区间整数)。Node 半的归一与浏览器半的表单共用这一格:
|
|
95
|
+
* 两处各写一份范围就会「表单说能存、宿主激活时说越界」。 */
|
|
96
|
+
const CONFIG_LIMITS = {
|
|
97
|
+
orphanPreviewLimit: {
|
|
98
|
+
min: 1,
|
|
99
|
+
max: 1e4
|
|
100
|
+
},
|
|
101
|
+
maxAttachmentBytes: {
|
|
102
|
+
min: 1,
|
|
103
|
+
max: 209715200
|
|
104
|
+
},
|
|
105
|
+
maxJsonRefBytes: {
|
|
106
|
+
min: 1,
|
|
107
|
+
max: 104857600
|
|
108
|
+
}
|
|
109
|
+
};
|
|
88
110
|
/** 请求参数名:浏览器半发、Node 半读,同名同义(`empty` 没有入参)。
|
|
89
111
|
* 附件段是 **query 参数**(GET 二进制响应,没有请求体)。 */
|
|
90
112
|
const PARAMS = {
|
|
@@ -2158,6 +2180,7 @@ async function cleanOrphanGroup(deps, group) {
|
|
|
2158
2180
|
//#endregion
|
|
2159
2181
|
//#region src/ports/agent-handles.ts
|
|
2160
2182
|
const CAPTURED_AGENT_HANDLES = Symbol.for("dsh-session-delete.captured-agent-handles");
|
|
2183
|
+
const CAPTURED_AGENT_WRAPPERS = Symbol.for("dsh-session-delete.captured-agent-wrappers");
|
|
2161
2184
|
/** 在 `ctx.agents` 上包一层 `resume` / `create`,把返回的 agent handle 的 dispose 按会话 id 记下。
|
|
2162
2185
|
* 服务缺席时走 `ctx.inject(['agents'])` **等服务挂上再装**——live profile 的插件激活顺序可能早于
|
|
2163
2186
|
* `agents` 服务就绪;只在 apply 时 `get` 一次会让捕获静默缺席(真机症状:重启前在回收站的能冷恢复,
|
|
@@ -2166,14 +2189,46 @@ const CAPTURED_AGENT_HANDLES = Symbol.for("dsh-session-delete.captured-agent-han
|
|
|
2166
2189
|
* **安装时机**:先 `get('agents')`,没装成再 `inject(['agents'])`(两者只装一次,符号表兜住)。
|
|
2167
2190
|
* **symbol 表**:句柄表挂在服务实例的 {@link CAPTURED_AGENT_HANDLES} 上;同一个 agents 服务上的第二次
|
|
2168
2191
|
* 安装(`get` 与 `inject` 各一次,或第二个 `createHostPort`)看见它就直接共用,不把 wrapper 套第二层。
|
|
2192
|
+
* **安装者拥有 wrapper**:包装记录先在闭包里留一份(`ownWrappers`,建立于 symbol 写入之前),同时挂到
|
|
2193
|
+
* {@link CAPTURED_AGENT_WRAPPERS};第一次成功安装时经 `ctx.effect` 注册一条卸载恢复——按 own property
|
|
2194
|
+
* descriptor 的身份把 `resume` / `create` 写回目标自有的原函数(`restoreTo`;代理读回的影子不写回,
|
|
2195
|
+
* 别人后装的 wrapper 不动),并摘掉仍归本闭包的 symbol 表。恢复是总的:一条写回失败不跳过其余记录。
|
|
2196
|
+
* 恢复一次即锁死:晚到的安装与晚到的 handle 落地都被 `restored` 挡住。没有 `ctx.effect`(假件 / 旧 ctx)
|
|
2197
|
+
* 时包装照装,只是没有恢复。
|
|
2169
2198
|
* **读回判据**:cordis 的 traceable 服务代理每次读函数都会新包一层(`get` 里的 `createShadowMethod`),
|
|
2170
2199
|
* 所以只能看 own property descriptor 的身份;拿 `service[name]` 读回比身份是必然的假警报。
|
|
2171
2200
|
*/
|
|
2172
2201
|
function captureAgentHandles(ctx) {
|
|
2173
2202
|
let handles = /* @__PURE__ */ new Map();
|
|
2203
|
+
/** 本闭包装上的包装记录:symbol 写入是 best-effort,恢复不能只靠它。 */
|
|
2204
|
+
let ownWrappers;
|
|
2174
2205
|
let installed = false;
|
|
2206
|
+
/** 卸载恢复只跑一次;同时是「清理之后不再安装、不再捕获」的闸。 */
|
|
2207
|
+
let restored = false;
|
|
2208
|
+
/**
|
|
2209
|
+
* 卸载恢复:按记录倒序把仍是我们装的方法写回 `restoreTo`,再摘掉仍是本闭包那张表的 symbol 并清空
|
|
2210
|
+
* 句柄表。身份判据挡两件事:别人后装的 wrapper 不被覆盖;别人换过的 symbol 表不被误删。
|
|
2211
|
+
* 恢复必须走完:一条写回失败只跳过那一条,摘 symbol 与清表在 `finally` 里照做。
|
|
2212
|
+
*/
|
|
2213
|
+
const restore = (service) => {
|
|
2214
|
+
if (restored) return;
|
|
2215
|
+
restored = true;
|
|
2216
|
+
const records = ownWrappers ?? service[CAPTURED_AGENT_WRAPPERS] ?? [];
|
|
2217
|
+
try {
|
|
2218
|
+
for (const record of [...records].reverse()) try {
|
|
2219
|
+
if (Object.getOwnPropertyDescriptor(service, record.name)?.value === record.wrapped) service[record.name] = record.restoreTo;
|
|
2220
|
+
} catch {}
|
|
2221
|
+
} finally {
|
|
2222
|
+
try {
|
|
2223
|
+
if (service[CAPTURED_AGENT_HANDLES] === handles) Reflect.deleteProperty(service, CAPTURED_AGENT_HANDLES);
|
|
2224
|
+
if (service[CAPTURED_AGENT_WRAPPERS] === records) Reflect.deleteProperty(service, CAPTURED_AGENT_WRAPPERS);
|
|
2225
|
+
} finally {
|
|
2226
|
+
handles.clear();
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2175
2230
|
const install = (candidate) => {
|
|
2176
|
-
if (installed) return;
|
|
2231
|
+
if (installed || restored) return;
|
|
2177
2232
|
const service = candidate;
|
|
2178
2233
|
if (service === void 0 || service === null) return;
|
|
2179
2234
|
const existing = service[CAPTURED_AGENT_HANDLES];
|
|
@@ -2182,13 +2237,17 @@ function captureAgentHandles(ctx) {
|
|
|
2182
2237
|
installed = true;
|
|
2183
2238
|
return;
|
|
2184
2239
|
}
|
|
2240
|
+
const wrappers = [];
|
|
2185
2241
|
let wrappedCount = 0;
|
|
2186
2242
|
for (const name of ["resume", "create"]) try {
|
|
2187
2243
|
const original = service[name];
|
|
2188
2244
|
if (typeof original !== "function") continue;
|
|
2245
|
+
const own = Object.getOwnPropertyDescriptor(service, name)?.value;
|
|
2246
|
+
const restoreTo = typeof own === "function" ? own : original;
|
|
2189
2247
|
const wrapped = function(...args) {
|
|
2190
2248
|
const result = Reflect.apply(original, this, args);
|
|
2191
2249
|
result.then((value) => {
|
|
2250
|
+
if (restored) return;
|
|
2192
2251
|
const handle = value;
|
|
2193
2252
|
const id = handle?.agent?.id;
|
|
2194
2253
|
const dispose = handle?.dispose;
|
|
@@ -2201,13 +2260,24 @@ function captureAgentHandles(ctx) {
|
|
|
2201
2260
|
};
|
|
2202
2261
|
service[name] = wrapped;
|
|
2203
2262
|
if (Object.getOwnPropertyDescriptor(service, name)?.value !== wrapped) throw new Error(`agents.${name} is not writable`);
|
|
2263
|
+
wrappers.push({
|
|
2264
|
+
name,
|
|
2265
|
+
original,
|
|
2266
|
+
restoreTo,
|
|
2267
|
+
wrapped
|
|
2268
|
+
});
|
|
2204
2269
|
wrappedCount += 1;
|
|
2205
2270
|
} catch {}
|
|
2206
2271
|
if (wrappedCount === 0) return;
|
|
2272
|
+
ownWrappers = wrappers;
|
|
2207
2273
|
try {
|
|
2208
2274
|
service[CAPTURED_AGENT_HANDLES] = handles;
|
|
2275
|
+
service[CAPTURED_AGENT_WRAPPERS] = wrappers;
|
|
2209
2276
|
} catch {}
|
|
2210
2277
|
installed = true;
|
|
2278
|
+
try {
|
|
2279
|
+
ctx.effect?.(() => () => restore(service), "session-delete.agent-handles");
|
|
2280
|
+
} catch {}
|
|
2211
2281
|
};
|
|
2212
2282
|
const service = ctx.get("agents");
|
|
2213
2283
|
if (service !== void 0 && service !== null) install(service);
|
|
@@ -2832,6 +2902,111 @@ function applyActions(deps, action, ids) {
|
|
|
2832
2902
|
}
|
|
2833
2903
|
}
|
|
2834
2904
|
//#endregion
|
|
2905
|
+
//#region src/config.ts
|
|
2906
|
+
/**
|
|
2907
|
+
* 可调项(Config,ADR-0037):三只旋钮 + 本地 Standard Schema。
|
|
2908
|
+
*
|
|
2909
|
+
* 为什么不用 `@deepseek-ai/schemastery` 写 schema:那是宿主包,值 import 会破本仓「跨插件一律
|
|
2910
|
+
* `import type`」的硬约束;Cordis 的 Config 接受 Standard Schema(`@standard-schema/spec`),
|
|
2911
|
+
* 手写这一小段即可,不需要把宿主 schema 库搬进插件。代价是宿主只为 native Schemastery schema 投影
|
|
2912
|
+
* 字段表单,这枚手写的 Standard Schema 不在其列——字段表单由插件设置页的「可调项」卡提供
|
|
2913
|
+
* (ADR-0043),profile 的 `cordis.patch.yml` 仍是默认值与覆盖的家。
|
|
2914
|
+
*
|
|
2915
|
+
* 这里只做**默认值、归一与 schema**,不持有运行期现场:`apply` 归一一次,结果作为
|
|
2916
|
+
* `CoreDeps.config` 注入;读方从自己的 deps 读,不 import 模块级单例。
|
|
2917
|
+
* 客户端 6 秒撤销时长不在这里:它是浏览器半的产品决定,不跨半传。
|
|
2918
|
+
*/
|
|
2919
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
2920
|
+
orphanPreviewLimit: 200,
|
|
2921
|
+
maxAttachmentBytes: 20971520,
|
|
2922
|
+
maxJsonRefBytes: 10485760
|
|
2923
|
+
});
|
|
2924
|
+
function intField(input, key, range) {
|
|
2925
|
+
const value = input[key];
|
|
2926
|
+
if (value === void 0) return DEFAULT_CONFIG[key];
|
|
2927
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < range.min || value > range.max) throw new Error(`配置项 ${key} 需为 ${range.min}..${range.max} 的整数`);
|
|
2928
|
+
return value;
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* 把 patch 里的 `config` 归一成完整配置:缺省取默认值,未知键 / 非整数 / 越界都抛。
|
|
2932
|
+
* 抛错会让插件激活失败(响亮),而不是悄悄用半套配置跑。
|
|
2933
|
+
*/
|
|
2934
|
+
function normalizeConfig(value) {
|
|
2935
|
+
if (value === void 0 || value === null) return DEFAULT_CONFIG;
|
|
2936
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new Error("配置需为对象");
|
|
2937
|
+
const input = value;
|
|
2938
|
+
const known = new Set(Object.keys(CONFIG_LIMITS));
|
|
2939
|
+
for (const key of Object.keys(input)) if (!known.has(key)) throw new Error(`未知配置项:${key}`);
|
|
2940
|
+
return {
|
|
2941
|
+
orphanPreviewLimit: intField(input, "orphanPreviewLimit", CONFIG_LIMITS.orphanPreviewLimit),
|
|
2942
|
+
maxAttachmentBytes: intField(input, "maxAttachmentBytes", CONFIG_LIMITS.maxAttachmentBytes),
|
|
2943
|
+
maxJsonRefBytes: intField(input, "maxJsonRefBytes", CONFIG_LIMITS.maxJsonRefBytes)
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* 插件 Config:Cordis 接受 Standard Schema,`~standard.validate` 交回归一后的配置。
|
|
2948
|
+
* 宿主在激活前校验;`apply` 再归一一次(同一份事实,不会漂)。
|
|
2949
|
+
*/
|
|
2950
|
+
const Config = { "~standard": {
|
|
2951
|
+
version: 1,
|
|
2952
|
+
vendor: "dsh-session-delete",
|
|
2953
|
+
validate(value) {
|
|
2954
|
+
try {
|
|
2955
|
+
return { value: normalizeConfig(value) };
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
return { issues: [{ message: error instanceof Error ? error.message : String(error) }] };
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
} };
|
|
2961
|
+
//#endregion
|
|
2962
|
+
//#region src/verbs/config.ts
|
|
2963
|
+
/**
|
|
2964
|
+
* `/config` 的读/写动词:三只可调项的当前值与写回。
|
|
2965
|
+
*
|
|
2966
|
+
* 读是免费快照(`apply` 归一后的那一份),写才碰宿主:
|
|
2967
|
+
* - 先按 `src/config.ts` 的 `normalizeConfig` 校验(未知键 / 非整数 / 越界回 400,不拿半套值写盘);
|
|
2968
|
+
* - 再走宿主 `configEditor.edit(entry, change)`:完整 raw config 落进 profile 的 patch 层,
|
|
2969
|
+
* 由编辑器串行化、校验并让这一行重启(字段不是 volatile,重启是它给的语义)。
|
|
2970
|
+
* - `configEditor` / `entry` 缺席(老宿主、嵌套上下文)回 `io` 并在原文里说清替代路径——
|
|
2971
|
+
* 不静默假装保存成功。
|
|
2972
|
+
*
|
|
2973
|
+
* `entry` 由组合根从 `ctx.fiber.entry` 取(见 `src/index.ts`);本文件不碰 ctx。
|
|
2974
|
+
*/
|
|
2975
|
+
/** `/config` 的请求体:`null` 或 `{}` 是读;只要带一个键就当写(未知键由 {@link normalizeConfig} 400 拒掉)。
|
|
2976
|
+
* 路由表按同一句话定纯度,所以别在别处再写一遍这个判据。 */
|
|
2977
|
+
function configPatchOf(body) {
|
|
2978
|
+
if (body === null || Object.keys(body).length === 0) return void 0;
|
|
2979
|
+
return body;
|
|
2980
|
+
}
|
|
2981
|
+
/** 空体 = 读当前值;带字段 = 写。请求体已经由适配器解析成对象或 null。 */
|
|
2982
|
+
function readOrSaveConfig(deps, body) {
|
|
2983
|
+
const patch = configPatchOf(body);
|
|
2984
|
+
if (patch === void 0) return deps.config;
|
|
2985
|
+
return saveConfig(deps, patch);
|
|
2986
|
+
}
|
|
2987
|
+
async function saveConfig(deps, patch) {
|
|
2988
|
+
let next;
|
|
2989
|
+
try {
|
|
2990
|
+
next = normalizeConfig({
|
|
2991
|
+
...deps.config,
|
|
2992
|
+
...patch
|
|
2993
|
+
});
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
throw routeError("bad-request", 400, error instanceof Error ? error.message : String(error));
|
|
2996
|
+
}
|
|
2997
|
+
const editor = deps.configEditor;
|
|
2998
|
+
if (editor === void 0 || deps.entry === void 0) throw verbError("io", "这一代宿主不能保存配置;请直接改 profile 的 cordis.patch.yml");
|
|
2999
|
+
try {
|
|
3000
|
+
await editor.edit(deps.entry, (current) => ({
|
|
3001
|
+
...current,
|
|
3002
|
+
...next
|
|
3003
|
+
}));
|
|
3004
|
+
} catch (error) {
|
|
3005
|
+
throw verbError("io", `配置没有保存:${error instanceof Error ? error.message : String(error)}`, error);
|
|
3006
|
+
}
|
|
3007
|
+
return next;
|
|
3008
|
+
}
|
|
3009
|
+
//#endregion
|
|
2835
3010
|
//#region src/trash/detail.ts
|
|
2836
3011
|
/**
|
|
2837
3012
|
* 回收站**单条详情**:从回收站目录里那条会话自己的日志读出元信息与正文块(用例见 `tests/trash-detail.test.ts`)。
|
|
@@ -3599,7 +3774,7 @@ function requireIdArray(body) {
|
|
|
3599
3774
|
return value;
|
|
3600
3775
|
}
|
|
3601
3776
|
/**
|
|
3602
|
-
*
|
|
3777
|
+
* 十个段名各一行。`Record<段名, AnyRouteSpec>` 是闸:`wire.ts` 加了段名而这里没加,编译不过;
|
|
3603
3778
|
* 每行的 `slice` 又把该行的 `run` 钉在自己那一格 deps 上(ADR-0032)。
|
|
3604
3779
|
*/
|
|
3605
3780
|
const ROUTE_TABLE = {
|
|
@@ -3621,6 +3796,15 @@ const ROUTE_TABLE = {
|
|
|
3621
3796
|
response: "json",
|
|
3622
3797
|
run: async () => "pong"
|
|
3623
3798
|
},
|
|
3799
|
+
[ROUTES.config.path]: {
|
|
3800
|
+
slice: "config",
|
|
3801
|
+
method: ROUTES.config.method,
|
|
3802
|
+
body: "json",
|
|
3803
|
+
purity: (body) => configPatchOf(body) === void 0 ? "pure" : "chained",
|
|
3804
|
+
read: "none",
|
|
3805
|
+
response: "json",
|
|
3806
|
+
run: async (deps, body) => readOrSaveConfig(deps, body)
|
|
3807
|
+
},
|
|
3624
3808
|
[ROUTES.attachment.path]: {
|
|
3625
3809
|
slice: "attachment",
|
|
3626
3810
|
method: ROUTES.attachment.method,
|
|
@@ -3704,7 +3888,7 @@ function routeOf(rest) {
|
|
|
3704
3888
|
/**
|
|
3705
3889
|
* /dsh-session-delete-api 的分发核心:只负责路由记录的执行。
|
|
3706
3890
|
*
|
|
3707
|
-
* 传输适配器只有 `src/api/connection-route.ts`
|
|
3891
|
+
* 传输适配器只有 `src/api/connection-route.ts` 一个:十段注册到宿主 `connection.fetch.register`
|
|
3708
3892
|
* 的 exact Fetch 路由(`Request` / `Response`),Host/Origin/浏览器认证由宿主 `/api` bridge 完成
|
|
3709
3893
|
* (ADR-0033)。`Request` 的字节闸 / JSON 解析、`Response` 的头与失败信封都归适配器;
|
|
3710
3894
|
* 这里只收已经解析好的请求,做查表 → 按段注入窄面 → 跑处理 → 回结果。
|
|
@@ -3764,7 +3948,7 @@ function removalOf(full) {
|
|
|
3764
3948
|
config: full.config
|
|
3765
3949
|
};
|
|
3766
3950
|
}
|
|
3767
|
-
/**
|
|
3951
|
+
/** 五格只建一次:收窄对象在 dispatcher 构造期固定,请求路径只做引用选择。 */
|
|
3768
3952
|
function slicesOf(full) {
|
|
3769
3953
|
const core = coreOf(full);
|
|
3770
3954
|
return {
|
|
@@ -3774,6 +3958,11 @@ function slicesOf(full) {
|
|
|
3774
3958
|
attachment: {
|
|
3775
3959
|
...core,
|
|
3776
3960
|
attachments: full.attachments
|
|
3961
|
+
},
|
|
3962
|
+
config: {
|
|
3963
|
+
...core,
|
|
3964
|
+
...full.configEditor === void 0 ? {} : { configEditor: full.configEditor },
|
|
3965
|
+
...full.entry === void 0 ? {} : { entry: full.entry }
|
|
3777
3966
|
}
|
|
3778
3967
|
};
|
|
3779
3968
|
}
|
|
@@ -3824,7 +4013,7 @@ function createDispatcher(deps) {
|
|
|
3824
4013
|
//#endregion
|
|
3825
4014
|
//#region src/api/connection-route.ts
|
|
3826
4015
|
/**
|
|
3827
|
-
* 现代宿主的 exact Fetch
|
|
4016
|
+
* 现代宿主的 exact Fetch 路由适配器:把十段注册到 `connection.fetch.register`,
|
|
3828
4017
|
* 由宿主 `/api` bridge 做 Host/Origin/浏览器认证(ADR-0033)。
|
|
3829
4018
|
*
|
|
3830
4019
|
* 为什么路径必须带 `/api`:宿主 `assertFetchRoute` 用 `endpointFromPath('/api', path)` 校验,
|
|
@@ -3963,7 +4152,7 @@ function toResponse(result) {
|
|
|
3963
4152
|
});
|
|
3964
4153
|
}
|
|
3965
4154
|
/**
|
|
3966
|
-
*
|
|
4155
|
+
* 注册十段 exact Fetch 路由。返回一个组合 disposer;调用方把它挂在自己的 effect 上。
|
|
3967
4156
|
* `connection` 是插件的硬依赖(web UI / 桌面端都装 `client-connection`);`fetch.register`
|
|
3968
4157
|
* 缺席属于宿主形状漂移,直接抛——静默返回空 disposer 会让插件「在但没有入口」。
|
|
3969
4158
|
* `dispatch` 与分发器同源。
|
|
@@ -4007,64 +4196,6 @@ function registerConnectionRoutes(service, dispatch) {
|
|
|
4007
4196
|
return disposeAll;
|
|
4008
4197
|
}
|
|
4009
4198
|
//#endregion
|
|
4010
|
-
//#region src/config.ts
|
|
4011
|
-
const DEFAULT_CONFIG = Object.freeze({
|
|
4012
|
-
orphanPreviewLimit: 200,
|
|
4013
|
-
maxAttachmentBytes: 20971520,
|
|
4014
|
-
maxJsonRefBytes: 10485760
|
|
4015
|
-
});
|
|
4016
|
-
const LIMITS = {
|
|
4017
|
-
orphanPreviewLimit: {
|
|
4018
|
-
min: 1,
|
|
4019
|
-
max: 1e4
|
|
4020
|
-
},
|
|
4021
|
-
maxAttachmentBytes: {
|
|
4022
|
-
min: 1,
|
|
4023
|
-
max: 209715200
|
|
4024
|
-
},
|
|
4025
|
-
maxJsonRefBytes: {
|
|
4026
|
-
min: 1,
|
|
4027
|
-
max: 104857600
|
|
4028
|
-
}
|
|
4029
|
-
};
|
|
4030
|
-
function intField(input, key, range) {
|
|
4031
|
-
const value = input[key];
|
|
4032
|
-
if (value === void 0) return DEFAULT_CONFIG[key];
|
|
4033
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < range.min || value > range.max) throw new Error(`配置项 ${key} 需为 ${range.min}..${range.max} 的整数`);
|
|
4034
|
-
return value;
|
|
4035
|
-
}
|
|
4036
|
-
/**
|
|
4037
|
-
* 把 patch 里的 `config` 归一成完整配置:缺省取默认值,未知键 / 非整数 / 越界都抛。
|
|
4038
|
-
* 抛错会让插件激活失败(响亮),而不是悄悄用半套配置跑。
|
|
4039
|
-
*/
|
|
4040
|
-
function normalizeConfig(value) {
|
|
4041
|
-
if (value === void 0 || value === null) return DEFAULT_CONFIG;
|
|
4042
|
-
if (typeof value !== "object" || Array.isArray(value)) throw new Error("配置需为对象");
|
|
4043
|
-
const input = value;
|
|
4044
|
-
const known = new Set(Object.keys(LIMITS));
|
|
4045
|
-
for (const key of Object.keys(input)) if (!known.has(key)) throw new Error(`未知配置项:${key}`);
|
|
4046
|
-
return {
|
|
4047
|
-
orphanPreviewLimit: intField(input, "orphanPreviewLimit", LIMITS.orphanPreviewLimit),
|
|
4048
|
-
maxAttachmentBytes: intField(input, "maxAttachmentBytes", LIMITS.maxAttachmentBytes),
|
|
4049
|
-
maxJsonRefBytes: intField(input, "maxJsonRefBytes", LIMITS.maxJsonRefBytes)
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
/**
|
|
4053
|
-
* 插件 Config:Cordis 接受 Standard Schema,`~standard.validate` 交回归一后的配置。
|
|
4054
|
-
* 宿主在激活前校验;`apply` 再归一一次(同一份事实,不会漂)。
|
|
4055
|
-
*/
|
|
4056
|
-
const Config = { "~standard": {
|
|
4057
|
-
version: 1,
|
|
4058
|
-
vendor: "dsh-session-delete",
|
|
4059
|
-
validate(value) {
|
|
4060
|
-
try {
|
|
4061
|
-
return { value: normalizeConfig(value) };
|
|
4062
|
-
} catch (error) {
|
|
4063
|
-
return { issues: [{ message: error instanceof Error ? error.message : String(error) }] };
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
} };
|
|
4067
|
-
//#endregion
|
|
4068
4199
|
//#region src/ports/home.ts
|
|
4069
4200
|
/**
|
|
4070
4201
|
* 宿主数据根。官方解析器在 asar `dsh/node_modules/@deepseek-ai/dsh-home-paths/lib/index.js`:
|
|
@@ -4330,7 +4461,7 @@ const name = "@xrn1997/dsh-session-delete";
|
|
|
4330
4461
|
* - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
|
|
4331
4462
|
* 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
|
|
4332
4463
|
*
|
|
4333
|
-
* `connection`
|
|
4464
|
+
* `connection` 是浏览器半取数通道的唯一承载者:十段注册到 `connection.fetch.register` 的
|
|
4334
4465
|
* `/api/dsh-session-delete-api/*`,Host/Origin/浏览器认证由宿主 `/api` bridge 完成(ADR-0033)。
|
|
4335
4466
|
* 支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`;服务缺席时插件停在 pending
|
|
4336
4467
|
* ——可见的激活失败,而不是静默地没有入口。`webServer` 不再直接使用:exact route 仍由宿主的
|
|
@@ -4358,6 +4489,8 @@ function apply(ctx, rawConfig) {
|
|
|
4358
4489
|
const home = resolveHome();
|
|
4359
4490
|
const rawManifest = createHostStorageManifest(ctx);
|
|
4360
4491
|
const host = createHostPort(ctx);
|
|
4492
|
+
const configEditor = ctx.get("configEditor");
|
|
4493
|
+
const entry = ctx.fiber?.entry;
|
|
4361
4494
|
const deps = {
|
|
4362
4495
|
fs,
|
|
4363
4496
|
host,
|
|
@@ -4366,7 +4499,9 @@ function apply(ctx, rawConfig) {
|
|
|
4366
4499
|
presence: createMemberPresence(host),
|
|
4367
4500
|
config,
|
|
4368
4501
|
attachments: createAttachmentsPort(ctx),
|
|
4369
|
-
now: () => Date.now()
|
|
4502
|
+
now: () => Date.now(),
|
|
4503
|
+
...configEditor === void 0 ? {} : { configEditor },
|
|
4504
|
+
...entry === void 0 ? {} : { entry }
|
|
4370
4505
|
};
|
|
4371
4506
|
const connection = ctx.get("connection");
|
|
4372
4507
|
if (connection === void 0) throw new Error("session-delete: connection 服务缺席");
|
|
@@ -15,7 +15,7 @@ export interface ConnectionFetchServiceLike {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* 注册十段 exact Fetch 路由。返回一个组合 disposer;调用方把它挂在自己的 effect 上。
|
|
19
19
|
* `connection` 是插件的硬依赖(web UI / 桌面端都装 `client-connection`);`fetch.register`
|
|
20
20
|
* 缺席属于宿主形状漂移,直接抛——静默返回空 disposer 会让插件「在但没有入口」。
|
|
21
21
|
* `dispatch` 与分发器同源。
|
|
@@ -18,7 +18,7 @@ export type ApiResult = {
|
|
|
18
18
|
};
|
|
19
19
|
/** 段名的 404 与方法的 405 只在这里判一次;空段名按插件前缀显示,避免 404 原文出现一个空串。 */
|
|
20
20
|
export declare function resolveRoute(method: string, rest: string): RouteSpec;
|
|
21
|
-
/**
|
|
21
|
+
/** 五格只建一次:收窄对象在 dispatcher 构造期固定,请求路径只做引用选择。 */
|
|
22
22
|
export declare function slicesOf(full: DeleteDeps): SliceDeps;
|
|
23
23
|
/** 分发函数:查段 → 方法闸 → 读面 → 处理。 */
|
|
24
24
|
export type Dispatch = (input: ApiInput) => Promise<ApiResult>;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 这张表只回答“怎么处理”。
|
|
13
13
|
*/
|
|
14
14
|
import { type ApplyAction, type RouteName } from '../shared/wire.js';
|
|
15
|
-
import type { CoreDeps, DeleteDeps, RemovalDeps } from '../deps.js';
|
|
15
|
+
import type { ConfigEditorLike, CoreDeps, DeleteDeps, RemovalDeps } from '../deps.js';
|
|
16
16
|
import type { AttachmentsPort } from '../ports/attachments-port.js';
|
|
17
17
|
import type { TrashEntry } from '../trash/entry.js';
|
|
18
18
|
/** 请求体:`json` 才读下来(GET 一律不读;读两遍会把流消费掉)。 */
|
|
@@ -35,9 +35,10 @@ export interface RouteRead {
|
|
|
35
35
|
rows(): Promise<readonly TrashEntry[]>;
|
|
36
36
|
}
|
|
37
37
|
/** deps 切片(ADR-0032):每一段只声明自己要的那一面。读与探测五段拿基础面,`attachment` 段另多一条
|
|
38
|
-
* 宿主 `attachments` 读面,两个删盘段多删除能力,`/apply`
|
|
39
|
-
*
|
|
40
|
-
|
|
38
|
+
* 宿主 `attachments` 读面,两个删盘段多删除能力,`/apply` 是唯一拿全量面的行,`/config` 只多
|
|
39
|
+
* configEditor + entry 两件(段的 run 里长出超面调用就是编译错;dispatch 也只按这一格注入,
|
|
40
|
+
* 运行期收窄住 `src/api/dispatch.ts` 的 `slicesOf`)。 */
|
|
41
|
+
export type RouteSlice = 'core' | 'removal' | 'full' | 'attachment' | 'config';
|
|
41
42
|
/** 切片名 → 依赖面:{@link RouteSpec.run} 的 deps 参数类型与 `depsFor` 的返回类型都从这一格取。 */
|
|
42
43
|
export type SliceDeps = {
|
|
43
44
|
core: CoreDeps;
|
|
@@ -46,6 +47,10 @@ export type SliceDeps = {
|
|
|
46
47
|
attachment: CoreDeps & {
|
|
47
48
|
readonly attachments: AttachmentsPort;
|
|
48
49
|
};
|
|
50
|
+
config: CoreDeps & {
|
|
51
|
+
readonly configEditor?: ConfigEditorLike;
|
|
52
|
+
readonly entry?: unknown;
|
|
53
|
+
};
|
|
49
54
|
};
|
|
50
55
|
interface BaseRouteSpec<S extends RouteSlice> {
|
|
51
56
|
/** 这一行吃哪一格 deps:dispatch 按它注入,`run` 的 deps 参数类型也跟着它走。 */
|
|
@@ -58,7 +63,7 @@ interface BaseRouteSpec<S extends RouteSlice> {
|
|
|
58
63
|
* 缺省不设闸——JSON 响应跨源读不到(没有 CORS),不需要它。 */
|
|
59
64
|
readonly fetchGate?: 'same-origin';
|
|
60
65
|
}
|
|
61
|
-
/** JSON 段:处理函数拿请求体与读面;`query`
|
|
66
|
+
/** JSON 段:处理函数拿请求体与读面;`query` 不在这里(十行里只有二进制段需要查询串)。 */
|
|
62
67
|
export interface JsonRouteSpec<S extends RouteSlice = RouteSlice> extends BaseRouteSpec<S> {
|
|
63
68
|
readonly response: 'json';
|
|
64
69
|
/** 声明成函数属性而不是方法:方法签名是双变的,显式标注更宽的 deps 能绕过切片闸;属性按逆变校验。 */
|
|
@@ -70,13 +75,13 @@ export interface BinaryRouteSpec<S extends RouteSlice = RouteSlice> extends Base
|
|
|
70
75
|
readonly run: (deps: SliceDeps[S], query: URLSearchParams) => Promise<BinaryResponse>;
|
|
71
76
|
}
|
|
72
77
|
export type RouteSpec<S extends RouteSlice = RouteSlice> = JsonRouteSpec<S> | BinaryRouteSpec<S>;
|
|
73
|
-
/**
|
|
78
|
+
/** 十行的并集(表的值类型):每行由自己的 `slice` 与 `response` 字面量定型,`run` 的 deps 面随之收窄。 */
|
|
74
79
|
export type AnyRouteSpec = {
|
|
75
80
|
[S in RouteSlice]: RouteSpec<S>;
|
|
76
81
|
}[RouteSlice];
|
|
77
82
|
export declare const APPLY_ACTIONS: readonly ApplyAction[];
|
|
78
83
|
/**
|
|
79
|
-
*
|
|
84
|
+
* 十个段名各一行。`Record<段名, AnyRouteSpec>` 是闸:`wire.ts` 加了段名而这里没加,编译不过;
|
|
80
85
|
* 每行的 `slice` 又把该行的 `run` 钉在自己那一格 deps 上(ADR-0032)。
|
|
81
86
|
*/
|
|
82
87
|
export declare const ROUTE_TABLE: Record<RouteName, AnyRouteSpec>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { type Translate } from './copy.js';
|
|
3
|
+
import type { ConfigDeps } from './remote.js';
|
|
4
|
+
export interface ConfigSettingsProps {
|
|
5
|
+
/** 整套 remote 的切片;测试可以只给维护那四件(缺这两件时本卡不渲染)。 */
|
|
6
|
+
readonly deps: Partial<ConfigDeps>;
|
|
7
|
+
readonly t?: Translate;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 卡片入口:两个方法都在才渲染。`useMaintenance` 与 `MaintenanceSettings` 的测试替身只造维护四件,
|
|
11
|
+
* 所以在那些用例里本卡整体缺席——不让"没给配置 deps"变成一次假渲染。
|
|
12
|
+
*/
|
|
13
|
+
export declare function ConfigSettings({ deps, t }: ConfigSettingsProps): ReactElement | null;
|
|
@@ -26,6 +26,9 @@ export interface ClientContextLike {
|
|
|
26
26
|
slots: SlotsLike;
|
|
27
27
|
/** 可选服务的读面(宿主自己的写法:`ctx.get('…')?.…`)。**缺省调用**:老宿主上没有这个方法。 */
|
|
28
28
|
get?(name: string): unknown;
|
|
29
|
+
/** 可选服务迟到时的安装面(`ctx.inject(['…'], child => …)`):服务就绪才回调,缺席就一直等。
|
|
30
|
+
* **缺省调用**:老宿主上没有这个方法。 */
|
|
31
|
+
inject?(deps: readonly string[], callback: (child: ClientContextLike) => unknown): unknown;
|
|
29
32
|
/** 挂一个随本插件卸载而撤销的副作用(返回的清理函数由框架接管)。
|
|
30
33
|
* 属性声明:测试会把它当成员读(`expect(ctx.effect).not.toHaveBeenCalled()`),实现不读 `this`。 */
|
|
31
34
|
effect: (execute: () => unknown, label?: string) => unknown;
|
|
@@ -25,6 +25,7 @@ declare const ZH: {
|
|
|
25
25
|
readonly 'trash.empty.hint': "在会话条目的「…」菜单里选「删除」,会话会先进到这里,随时可以恢复。";
|
|
26
26
|
readonly 'trash.summary': "{count} 个会话 · 可释放 {size}";
|
|
27
27
|
readonly 'trash.group.count': "{count} 个会话";
|
|
28
|
+
readonly 'trash.group.ungrouped': "未分组";
|
|
28
29
|
readonly 'trash.project.unknown': "未记录项目";
|
|
29
30
|
readonly 'trash.row.deletedNow': "刚刚删除";
|
|
30
31
|
readonly 'trash.row.deletedAgo': "删除于 {n}{unit}前";
|
|
@@ -37,7 +38,8 @@ declare const ZH: {
|
|
|
37
38
|
readonly 'trash.unit.months': "个月";
|
|
38
39
|
readonly 'trash.unit.years': "年";
|
|
39
40
|
readonly 'trash.row.restore': "恢复";
|
|
40
|
-
readonly 'trash.row.purge': "
|
|
41
|
+
readonly 'trash.row.purge': "彻底删除";
|
|
42
|
+
readonly 'trash.row.purgeOne': "彻底删除「{title}」";
|
|
41
43
|
readonly 'trash.footer.note': "回收站本体在宿主数据根下的 {dir} 里,不自动过期;清空后不可恢复。";
|
|
42
44
|
readonly 'trash.emptyAll': "清空回收站…";
|
|
43
45
|
readonly 'trash.confirm.all': "清空回收站?";
|
|
@@ -77,6 +79,22 @@ declare const ZH: {
|
|
|
77
79
|
readonly 'trash.attachments.skipped': "附件未清理:{reason}";
|
|
78
80
|
readonly 'trash.attachments.skippedPartial': "部分附件未清理:{reason}(已清理 {removed} 个)";
|
|
79
81
|
readonly 'trash.attachments.alsoFailed': ";另有 {count} 个附件删除失败";
|
|
82
|
+
readonly 'config.title': "可调项";
|
|
83
|
+
readonly 'config.hint': "保存后插件会重启;本页用保存回执回填,重开本页会重新读取。也可以直接改 profile 的 cordis.patch.yml。";
|
|
84
|
+
readonly 'config.orphanPreviewLimit': "孤儿附件预览条数";
|
|
85
|
+
readonly 'config.maxAttachmentBytes': "附件单次响应上限";
|
|
86
|
+
readonly 'config.maxJsonRefBytes': "附件引用扫描的单 JSON 上限";
|
|
87
|
+
readonly 'config.unit.items': "条";
|
|
88
|
+
readonly 'config.unit.bytes': "字节";
|
|
89
|
+
readonly 'config.loading': "正在读当前值…";
|
|
90
|
+
readonly 'config.loadFailed': "读取失败:{message}";
|
|
91
|
+
readonly 'config.save': "保存";
|
|
92
|
+
readonly 'config.saving': "保存中…";
|
|
93
|
+
readonly 'config.saved': "已保存;插件正在重启。";
|
|
94
|
+
readonly 'config.saveFailed': "保存失败:{message}";
|
|
95
|
+
readonly 'config.retry': "重试";
|
|
96
|
+
readonly 'config.invalid': "需为 {min}–{max} 的整数";
|
|
97
|
+
readonly 'config.unchanged': "没有改动";
|
|
80
98
|
readonly 'maintenance.title': "维护";
|
|
81
99
|
readonly 'maintenance.orphans.title': "孤儿会话";
|
|
82
100
|
readonly 'maintenance.orphans.hint': "父会话已不在会话列表或回收站里的子会话。检查只读,不会自动运行。";
|
|
@@ -105,8 +123,6 @@ declare const ZH: {
|
|
|
105
123
|
readonly 'detail.noBody': "这条会话没有可显示的内容";
|
|
106
124
|
readonly 'detail.field.created': "创建";
|
|
107
125
|
readonly 'detail.field.lastActivity': "最后活动";
|
|
108
|
-
readonly 'detail.field.deletedAt': "删除于";
|
|
109
|
-
readonly 'detail.deletedAt.combined': "{relative}({absolute})";
|
|
110
126
|
readonly 'detail.field.span': "活跃跨度";
|
|
111
127
|
readonly 'detail.field.size': "大小";
|
|
112
128
|
readonly 'detail.field.turns': "轮数";
|
|
@@ -138,8 +154,16 @@ declare const ZH: {
|
|
|
138
154
|
readonly 'detail.block.truncated': "已截断";
|
|
139
155
|
readonly 'detail.loadOlder': "加载更早";
|
|
140
156
|
readonly 'detail.loadOlder.loading': "正在加载更早…";
|
|
141
|
-
readonly 'detail.
|
|
142
|
-
readonly 'detail.
|
|
157
|
+
readonly 'detail.viewMode.label': "工作步骤展示";
|
|
158
|
+
readonly 'detail.viewMode.compact': "简洁";
|
|
159
|
+
readonly 'detail.viewMode.standard': "标准";
|
|
160
|
+
readonly 'detail.viewMode.detailed': "详细";
|
|
161
|
+
readonly 'detail.viewMode.verbose': "完全展开";
|
|
162
|
+
readonly 'detail.compact.empty': "这段会话只有过程内容;切到「标准」可以看到。";
|
|
163
|
+
readonly 'detail.compact.pageOnly': "这一页只有过程内容;点「加载更早」继续往前看。";
|
|
164
|
+
readonly 'detail.process.header': "工作步骤";
|
|
165
|
+
readonly 'detail.process.step': "{n} 步";
|
|
166
|
+
readonly 'detail.process.steps': "{n} 步";
|
|
143
167
|
readonly 'detail.injection.runtimeContext': "运行上下文";
|
|
144
168
|
readonly 'detail.injection.timeContext': "时间上下文";
|
|
145
169
|
readonly 'detail.injection.agentInstructions': "Agent 指令";
|
|
@@ -162,7 +186,8 @@ declare const ZH: {
|
|
|
162
186
|
readonly 'detail.attachment.saving': "保存中…";
|
|
163
187
|
readonly 'detail.attachment.saveFailed': "保存失败:{message}";
|
|
164
188
|
readonly 'detail.back': "← 返回清单";
|
|
165
|
-
readonly 'detail.subagents.header': "子会话({n}) · {size}";
|
|
189
|
+
readonly 'detail.subagents.header': "子会话({n}) · {size},随本条一起恢复或删除";
|
|
190
|
+
readonly 'detail.subagents.open': "查看子会话详情";
|
|
166
191
|
readonly 'detail.subagents.truncated': "仅列出前 {shown} 条,共 {total} 条";
|
|
167
192
|
readonly 'detail.backToParent': "← 返回父会话";
|
|
168
193
|
readonly 'detail.list.label': "回收站清单";
|
|
@@ -240,12 +265,14 @@ export declare function localeProbe(t: Translate): string;
|
|
|
240
265
|
*/
|
|
241
266
|
export declare function splitOn(sentence: string, inner: string): [string, string, string];
|
|
242
267
|
/**
|
|
243
|
-
* 绝对时间那一句串。清单行的 `title`、确认框里的meta
|
|
268
|
+
* 绝对时间那一句串。清单行的 `title`、确认框里的meta、详情的会话信息三处要的是同一个写法,
|
|
244
269
|
* 各自 `new Date(...).toLocaleString()` 一遍迟早会漂成两种样子。
|
|
245
270
|
*/
|
|
246
271
|
export declare function absoluteText(at: number): string;
|
|
247
272
|
/** 单位词本身(调用方把它填进各自语言的整句模板里,语序由各语言自己钉)。 */
|
|
248
273
|
export declare function unitWord(unit: RelativeTimeUnit, t: Translate): string;
|
|
274
|
+
/** 「工作步骤 N 步」:英文 n=1 要单数、中文两条同形;选哪条键只住这里,调用方只报数。 */
|
|
275
|
+
export declare function stepsText(n: number, t: Translate): string;
|
|
249
276
|
/** 「7分钟前」这类**不带谓语**的相对短语;`now` 桶不套模板——"刚刚前"不是话。 */
|
|
250
277
|
export declare function agoText(at: number, now: number, t: Translate): string;
|
|
251
278
|
export {};
|