@xrn1997/dsh-session-delete 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +165 -141
- package/cordis.patch.yml +7 -3
- package/icon.svg +18 -18
- package/lib/client.js +749 -430
- package/lib/index.js +850 -484
- package/lib/types/api/attachment-orphans.d.ts +9 -3
- package/lib/types/api/attachment.d.ts +17 -7
- package/lib/types/api/connection-route.d.ts +23 -0
- package/lib/types/api/dispatch.d.ts +24 -17
- package/lib/types/api/route-table.d.ts +25 -9
- package/lib/types/client/api-base.d.ts +19 -0
- package/lib/types/client/batch-bar.d.ts +1 -1
- package/lib/types/client/batch-pick.d.ts +19 -0
- package/lib/types/client/batch-select.d.ts +7 -5
- package/lib/types/client/context-menu-actions.d.ts +47 -0
- package/lib/types/client/copy.d.ts +8 -0
- package/lib/types/client/delete-confirm.d.ts +2 -2
- package/lib/types/client/host-rows.d.ts +27 -32
- package/lib/types/client/index.d.ts +2 -0
- package/lib/types/client/main-view.d.ts +2 -1
- package/lib/types/client/maintenance-settings.d.ts +3 -1
- package/lib/types/client/pending-open.d.ts +17 -0
- package/lib/types/client/remote.d.ts +22 -19
- package/lib/types/client/store.d.ts +1 -7
- package/lib/types/client/trash-nav.d.ts +5 -1
- package/lib/types/client/trash-view.d.ts +3 -0
- package/lib/types/client/ui/MenuItemButton.d.ts +1 -1
- package/lib/types/client/ui/Toast.d.ts +4 -1
- package/lib/types/client/use-trash-panel-controller.d.ts +1 -1
- package/lib/types/config.d.ts +45 -0
- package/lib/types/deps.d.ts +8 -3
- package/lib/types/domain/closure.d.ts +3 -0
- package/lib/types/index.d.ts +18 -16
- package/lib/types/ports/attachments-port.d.ts +40 -0
- package/lib/types/ports/fs-port.d.ts +10 -0
- package/lib/types/ports/host-port.d.ts +4 -2
- package/lib/types/shared/wire.d.ts +18 -5
- package/lib/types/trash/attachments.d.ts +17 -2
- package/lib/types/verbs/errors.d.ts +3 -1
- package/lib/types/verbs/orphan-subagents.d.ts +3 -2
- package/lib/types/verbs/permanent-removal.d.ts +10 -5
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +122 -117
- package/lib/types/api/request-trust.d.ts +0 -45
- package/lib/types/ports/connection-port.d.ts +0 -33
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `MenuItemButton`(**照抄** `@deepseek-ai/dsh-client-ui-primitives@0.2.0-rc.2` 的同名组件,出处与三条
|
|
3
3
|
* 改写规则见 `ui.css` 头注):菜单里的一行,宿主菜单容器渲染它、键盘巡游也认它。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* **形状必须与宿主自己的官方菜单行(pin/rename/fork/archive/session-id)逐字同构**:`<div class=itemWrap>` 里先是可选
|
|
6
6
|
* 的发丝线(`role="separator"`),再是 `<button type="button" role="menuitem">`,按钮内是
|
|
7
7
|
* 图标位 + 标签位(+ 官方的快捷键位)。少一层、换个标签,这一行就会在键盘顺序、hover 底色或
|
|
8
8
|
* `.itemLabel` 的省略号上跟邻行错开。
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* **砍掉的**:`anchor`(把横幅对齐到某个元素的水平中心——本插件的撤销提示就是窗口居中)、
|
|
14
14
|
* `actions[].prefix`(动作文案前的连接词,本插件只用「撤销」两个字)。**没砍 `text`/`icon`/`tone`/
|
|
15
15
|
* `holdMs`/`actions`/`onDone`**:唯一的调用点这几样都在用。
|
|
16
|
+
* **本插件加的**:`paused`(撤销在飞时冻结停留与淡出,失败原文要落回同一条横幅;官方没有这个开关)。
|
|
16
17
|
*/
|
|
17
18
|
import { type ReactElement, type ReactNode } from 'react';
|
|
18
19
|
import './styles.js';
|
|
@@ -32,7 +33,9 @@ export interface ToastProps {
|
|
|
32
33
|
holdMs?: number;
|
|
33
34
|
/** 句尾的行内动作(照着句子的顺序排)。 */
|
|
34
35
|
actions?: readonly ToastAction[] | undefined;
|
|
36
|
+
/** 暂停自动熄灭:这一趟还有下文(撤销在飞),先把横幅留着——失败原文要靠它上屏。 */
|
|
37
|
+
paused?: boolean | undefined;
|
|
35
38
|
/** 淡出完成时调用一次:在这里卸载。 */
|
|
36
39
|
onDone: () => void;
|
|
37
40
|
}
|
|
38
|
-
export declare function Toast({ text, icon, tone, holdMs, actions, onDone }: ToastProps): ReactElement;
|
|
41
|
+
export declare function Toast({ text, icon, tone, holdMs, actions, paused, onDone }: ToastProps): ReactElement;
|
|
@@ -43,7 +43,7 @@ export interface TrashPanelController {
|
|
|
43
43
|
readonly detailNav: DetailNav;
|
|
44
44
|
/** 打开一个孩子(父详情子会话段点进来):读它的详情,但清单选中不动。 */
|
|
45
45
|
openChild(child: TrashEntryWire): Promise<void>;
|
|
46
|
-
/**
|
|
46
|
+
/** 回到父详情(详情顶部那颗「返回父会话」;窄档 back/Esc 的第一程也走它)。 */
|
|
47
47
|
closeChild(): void;
|
|
48
48
|
readonly drilled: boolean;
|
|
49
49
|
readonly detail: TrashDetailState;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可调项(Config,ADR-0037):三只旋钮 + 本地 Standard Schema。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不用 `@deepseek-ai/schemastery` 写 schema:那是宿主包,值 import 会破本仓「跨插件一律
|
|
5
|
+
* `import type`」的硬约束;Cordis 的 Config 接受 Standard Schema(`@standard-schema/spec`),
|
|
6
|
+
* 手写这一小段即可,不需要把宿主 schema 库搬进插件。代价是宿主配置页拿不到字段级 JSON Schema
|
|
7
|
+
* ——配置以 profile 的 `cordis.patch.yml` 为准(README「可调项」有示例)。
|
|
8
|
+
*
|
|
9
|
+
* 这里只做**默认值、归一与 schema**,不持有运行期现场:`apply` 归一一次,结果作为
|
|
10
|
+
* `CoreDeps.config` 注入;读方从自己的 deps 读,不 import 模块级单例。
|
|
11
|
+
* 客户端 6 秒撤销时长不在这里:它是浏览器半的产品决定,不跨半传。
|
|
12
|
+
*/
|
|
13
|
+
export interface SessionDeleteConfig {
|
|
14
|
+
/** 设置页维护卡「孤儿附件」预览清单最多列多少条(数量与字节仍按全量报告)。 */
|
|
15
|
+
readonly orphanPreviewLimit: number;
|
|
16
|
+
/** `GET /attachment` 单次响应字节上限(服务路径与直接 fs fallback 共用)。 */
|
|
17
|
+
readonly maxAttachmentBytes: number;
|
|
18
|
+
/** 附件引用扫描里单个 JSON 引用源的字节上限;超过就按「读不出」fail closed。 */
|
|
19
|
+
readonly maxJsonRefBytes: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_CONFIG: SessionDeleteConfig;
|
|
22
|
+
/**
|
|
23
|
+
* 把 patch 里的 `config` 归一成完整配置:缺省取默认值,未知键 / 非整数 / 越界都抛。
|
|
24
|
+
* 抛错会让插件激活失败(响亮),而不是悄悄用半套配置跑。
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeConfig(value: unknown): SessionDeleteConfig;
|
|
27
|
+
/**
|
|
28
|
+
* 插件 Config:Cordis 接受 Standard Schema,`~standard.validate` 交回归一后的配置。
|
|
29
|
+
* 宿主在激活前校验;`apply` 再归一一次(同一份事实,不会漂)。
|
|
30
|
+
*/
|
|
31
|
+
export declare const Config: {
|
|
32
|
+
readonly '~standard': {
|
|
33
|
+
readonly version: 1;
|
|
34
|
+
readonly vendor: "dsh-session-delete";
|
|
35
|
+
readonly validate: (value: unknown) => {
|
|
36
|
+
value: SessionDeleteConfig;
|
|
37
|
+
issues?: never;
|
|
38
|
+
} | {
|
|
39
|
+
issues: {
|
|
40
|
+
message: string;
|
|
41
|
+
}[];
|
|
42
|
+
value?: never;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
package/lib/types/deps.d.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { CoreFsPort, FsPort } from './ports/fs-port.js';
|
|
2
2
|
import type { HostCorePort } from './ports/host-port.js';
|
|
3
|
+
import type { AttachmentsPort } from './ports/attachments-port.js';
|
|
3
4
|
import type { PresenceCapability } from './ports/presence-port.js';
|
|
5
|
+
import type { SessionDeleteConfig } from './config.js';
|
|
4
6
|
import type { TrashLedger } from './trash/ledger.js';
|
|
5
7
|
/** Node 半的基础依赖面:组合根只装配这一颗;动词之间不互相 import 依赖面 interface。
|
|
6
8
|
* `fs` 不含 `removeDir` / `removeFile`,`host` 不含在场三件——两条危险能力各走 {@link RemovalDeps} 与
|
|
7
|
-
* {@link CoreDeps.presence}
|
|
9
|
+
* {@link CoreDeps.presence}。`config` 是 `apply` 归一后的只读快照:读方从 deps 读,不 import 模块级单例。 */
|
|
8
10
|
export interface CoreDeps {
|
|
9
11
|
fs: CoreFsPort;
|
|
10
12
|
host: HostCorePort;
|
|
11
13
|
ledger: TrashLedger;
|
|
12
14
|
home: string;
|
|
13
15
|
presence: PresenceCapability;
|
|
16
|
+
config: SessionDeleteConfig;
|
|
14
17
|
}
|
|
15
18
|
/** 会话删除动词的依赖面:只多读时钟;它不删目录(回滚只把搬走的搬回)。 */
|
|
16
19
|
export type DeleteSessionDeps = CoreDeps & {
|
|
@@ -21,5 +24,7 @@ export type RemovalDeps = CoreDeps & {
|
|
|
21
24
|
fs: CoreFsPort & Pick<FsPort, 'removeDir' | 'removeFile'>;
|
|
22
25
|
};
|
|
23
26
|
/** 组合根装出的全量面:`/apply` 的分发同时够到删除的时钟与永久删除的 `removeDir` / `removeFile`
|
|
24
|
-
*
|
|
25
|
-
export type DeleteDeps = DeleteSessionDeps & RemovalDeps
|
|
27
|
+
* (后者也供孤儿附件清理段使用)。`attachments` 只给 `attachment` 读段(ADR-0035);动词不拿它。 */
|
|
28
|
+
export type DeleteDeps = DeleteSessionDeps & RemovalDeps & {
|
|
29
|
+
readonly attachments: AttachmentsPort;
|
|
30
|
+
};
|
|
@@ -33,6 +33,9 @@ export declare function firstByBare<N extends ClosureNode>(members: readonly N[]
|
|
|
33
33
|
* 已被前一组收走的那条,连同它整支子树都不再出现在这一组里——后代在那一组已被全部认领,所以
|
|
34
34
|
* 「丢掉已认领的」与「不走进已认领的那支」交回的是同一个集合。环上的代价是「少数一条」,
|
|
35
35
|
* 不是把一次扫描挂死。组序与成员序都按语料原序。
|
|
36
|
+
*
|
|
37
|
+
* 后代也必须是 subagent 起源:普通 fork 的 `parentSession` 只是来源记录,它不进组、也不穿过它收孩子
|
|
38
|
+
* (否则从 subagent 分出去的分支会被当成孤儿永久删掉)。
|
|
36
39
|
*/
|
|
37
40
|
export declare function orphanRootsOf<N extends ClosureNode & {
|
|
38
41
|
readonly parentSessionId?: string;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { Config } from './config.js';
|
|
2
3
|
export declare const name = "@xrn1997/dsh-session-delete";
|
|
4
|
+
/** 可调项的 Config(Standard Schema);为什么不用宿主 schema 库见 ADR-0037。 */
|
|
5
|
+
export { Config };
|
|
3
6
|
/**
|
|
4
7
|
* 插件依赖:**只写字符串数组**(隔离数据根上实测:写成 `{ required, optional }` 会让插件停在
|
|
5
8
|
* `pending (waiting for services: required, optional)`)。
|
|
@@ -7,31 +10,30 @@ export declare const name = "@xrn1997/dsh-session-delete";
|
|
|
7
10
|
* 三项都是在隔离数据根上实测注入成功、且本插件真正用到的宿主服务。**不进 inject 的**:
|
|
8
11
|
* - `remote`:那是**浏览器半**的服务(asar `dsh-api-gateway/lib/client.js`:`super(ctx, "remote")`),
|
|
9
12
|
* Node 半挂的是 `ctx.typertGateway`。把它写进 Node 半的 inject 会让插件永远 pending(同 `slots` 那次的形状)。
|
|
10
|
-
* - `sessions` / `sessionProjectionCache
|
|
11
|
-
* 用(活跃判据走 waterfall,见 src/ports/host-port.ts
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* - `sessions` / `sessionProjectionCache` / `sessionPersistence`:都按 `ctx.get(...)` **可选读**——
|
|
14
|
+
* `sessions` 给 `evictLive` 和 live header 用(活跃判据走 waterfall,见 src/ports/host-port.ts;
|
|
15
|
+
* 服务缺席回 `unknown`),`sessionProjectionCache` 是标题的投影读面,`sessionPersistence` 是
|
|
16
|
+
* `projectDirOf` / `titleOf` 的公开 header 读面(workspaceRegistry 硬注入它,标准组合必有)。
|
|
17
|
+
* inject 一个可能没挂的服务 = 整个插件 pending;标题取不到时 `titleOf` 返回空串,正是契约允许的降级。
|
|
14
18
|
* - `agents`:只给 handle 捕获用(`ctx.get('agents')`;缺席时 `ctx.inject(['agents'])` 等服务就绪再装)。
|
|
15
19
|
* 它同样可能晚于本插件激活,写进插件 inject 只会让整个插件 pending。
|
|
16
20
|
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
* `connection` 只喂请求栅栏的远程支(`src/api/request-trust.ts` ②),按 `ctx.get` 可选读:
|
|
23
|
-
* 服务缺席时插件照常激活,远程客户端 fail-closed。它不进 `inject`——写进去会让没有 web 组成的
|
|
24
|
-
* profile 整个停在 pending。
|
|
21
|
+
* `connection` 是浏览器半取数通道的唯一承载者:九段注册到 `connection.fetch.register` 的
|
|
22
|
+
* `/api/dsh-session-delete-api/*`,Host/Origin/浏览器认证由宿主 `/api` bridge 完成(ADR-0033)。
|
|
23
|
+
* 支持面是 web UI 与桌面端,二者的标准组合都装 `client-connection`;服务缺席时插件停在 pending
|
|
24
|
+
* ——可见的激活失败,而不是静默地没有入口。`webServer` 不再直接使用:exact route 仍由宿主的
|
|
25
|
+
* `connection` 挂在其 `/api` 前缀上,插件不需要自己碰物理服务器。
|
|
25
26
|
*
|
|
26
27
|
* 清册的三层各有各的家:raw 绑定 `src/trash/storage-manifest.ts`、读面(对账 + 读)`src/trash/ledger.ts`、
|
|
27
28
|
* 数据根 `src/ports/home.ts`。这里只把它们接起来,任何一层都不在组合根里长判据。
|
|
28
29
|
*/
|
|
29
30
|
export declare const inject: string[];
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* 装配顺序:**配置归一 → 清单 → 端口 → 在场能力 → 动词依赖 → 分发器 → exact Fetch 路由**
|
|
33
|
+
* (`DeleteDeps` 把这些依赖面合成一颗;`config` 是归一后的只读快照)。
|
|
32
34
|
*
|
|
33
35
|
* 路由用 `ctx.effect` 挂、disposer 归装载它的那根 fiber:插件卸载即摘路由(`register` 返回的
|
|
34
|
-
* disposer 是官方契约)。**同一进程里只注册一次**这件事由宿主把住:重复的 `(
|
|
35
|
-
* 在 `register`
|
|
36
|
+
* disposer 是官方契约)。**同一进程里只注册一次**这件事由宿主把住:重复的 `(path, method)`
|
|
37
|
+
* 在 `connection.fetch.register` 里直接抛,响亮失败而不是悄悄覆盖。
|
|
36
38
|
*/
|
|
37
|
-
export declare function apply(ctx: Context): void;
|
|
39
|
+
export declare function apply(ctx: Context, rawConfig?: unknown): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主 `attachments` 服务的窄镜像(ADR-0035):只声明本插件读取附件用到的两条。
|
|
3
|
+
*
|
|
4
|
+
* - 服务是**可选**的:`ctx.get('attachments')` 每次调用现查(服务可能晚于插件激活),
|
|
5
|
+
* `available()` 为假时调用方退回旧的直接 fs 路径。
|
|
6
|
+
* - `readImage` 会按记录里的引用**校验字节**;`readFileStream` 校验并分块读出。
|
|
7
|
+
* 两者都不在本文件里重写校验逻辑。
|
|
8
|
+
* - 不 import 宿主 `attachments` 服务的类型包:形状按该服务的运行期契约抄成窄镜像。
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
11
|
+
export type ImageMediaTypeLike = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
|
|
12
|
+
export interface ImageAttachmentRefLike {
|
|
13
|
+
readonly attachmentId: string;
|
|
14
|
+
readonly mediaType: ImageMediaTypeLike;
|
|
15
|
+
readonly bytes: number;
|
|
16
|
+
readonly width: number;
|
|
17
|
+
readonly height: number;
|
|
18
|
+
readonly name?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface FileAttachmentRefLike {
|
|
21
|
+
readonly attachmentId: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly bytes: number;
|
|
24
|
+
}
|
|
25
|
+
export interface StoredImageAttachmentLike {
|
|
26
|
+
readonly ref: ImageAttachmentRefLike;
|
|
27
|
+
readonly data: Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
export interface AttachmentsPort {
|
|
30
|
+
/** 服务在场且成员形状可用;调用方用它决定走服务还是直接 fs。 */
|
|
31
|
+
available(): boolean;
|
|
32
|
+
readImage(ref: ImageAttachmentRefLike): Promise<StoredImageAttachmentLike>;
|
|
33
|
+
readFileStream(ref: FileAttachmentRefLike): AsyncIterable<Uint8Array>;
|
|
34
|
+
}
|
|
35
|
+
/** 服务在 `available()` 之后消失/形状变了:调用方可以退回直接 fs,而不是把这次读取判成 io。 */
|
|
36
|
+
export declare class AttachmentsUnavailableError extends Error {
|
|
37
|
+
constructor(message?: string);
|
|
38
|
+
}
|
|
39
|
+
/** 每次调用惰性解析服务:插件与 `attachments` 的激活顺序没有承诺。 */
|
|
40
|
+
export declare function createAttachmentsPort(ctx: Context): AttachmentsPort;
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **缺席**只认 ENOENT 与 ENOTDIR("路径中间有一截不是目录"与"不存在"是同一种回答)。
|
|
3
|
+
*
|
|
4
|
+
* EACCES / EIO / ELOOP / 断挂载**不是缺席**:这些读数说明"我们看不见",不是"那里没有"。
|
|
5
|
+
* 破坏性动作(删目录、删附件对象、按"没有引用"清共享图)据此做决定,把读不出折成没有
|
|
6
|
+
* 就是数据消失;ADR-0039 是这条判据的裁决。
|
|
7
|
+
*/
|
|
8
|
+
export declare function isAbsentPath(error: unknown): boolean;
|
|
1
9
|
export interface FsPort {
|
|
2
10
|
/** 全部成员写成属性声明:端口方法会被**解构出来单独调**(`const { sizeOfDir } = port`,见下方
|
|
3
11
|
* 模块级递归的注释),实现体一律不读 `this`。方法简写的隐式 `this: FsPort` 会让
|
|
4
12
|
* `unbound-method` 把这种正常用法报成可疑;契约(返回的 Promise 形状)一字未动。 */
|
|
13
|
+
/** 路径在不在。**只有 ENOENT / ENOTDIR 回 `false`**;权限、盘挂、I/O 等读错误照抛——
|
|
14
|
+
* 调用方拿它当"缺席"判据时,读不出必须是失败,不是"没有"(ADR-0039)。 */
|
|
5
15
|
exists: (p: string) => Promise<boolean>;
|
|
6
16
|
/** 建目录(含中间层)。首删时 `~/.dsh/session-trash/` 还不存在,而 `rename` 不建父目录。 */
|
|
7
17
|
ensureDir: (p: string) => Promise<void>;
|
|
@@ -9,10 +9,12 @@ export interface HostIdentityPort {
|
|
|
9
9
|
* 宿主的「删除工作区」只删注册、保留目录与日志;恢复这类会话时跳过 attach,归入「未分组」。 */
|
|
10
10
|
workspaceExists(workspaceId: string): Promise<boolean>;
|
|
11
11
|
/** 会话落盘的**项目目录名**(`<sessions>/<projectKey(cwd)>`);账本里没有这个会话 ⇒ `undefined`。
|
|
12
|
-
*
|
|
12
|
+
* header 走公开 live/持久化读面;与 `workspaceOf` 同一口径:**未命中是正常的 no-op,不是故障**
|
|
13
|
+
* (存储真故障照抛)。 */
|
|
13
14
|
projectDirOf(sessionId: string): Promise<string | undefined>;
|
|
14
15
|
/** 删除时**尽力**取的会话标题;取不到返回空串,绝不因此让删除失败。
|
|
15
|
-
* 标题只存在于宿主自己的账本/缓存里(`FsPort` 不直读文件)⇒ 取数面归宿主端口,接官方投影缓存 API
|
|
16
|
+
* 标题只存在于宿主自己的账本/缓存里(`FsPort` 不直读文件)⇒ 取数面归宿主端口,接官方投影缓存 API;
|
|
17
|
+
* header 与 `projectDirOf` 走同一个公开读面。 */
|
|
16
18
|
titleOf(sessionId: string): Promise<string>;
|
|
17
19
|
isLive(sessionId: string): Promise<boolean>;
|
|
18
20
|
isArchived(sessionId: string): Promise<boolean>;
|
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* 信封 {ok:true,value} / {ok:false,error:{code,message,details?}} 也是两半的共同契约。
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 插件专属前缀(九段相对它命名)。
|
|
10
10
|
*
|
|
11
|
-
* 为什么必须带插件名:宿主
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* 为什么必须带插件名:宿主 `connection.fetch.register` 对**重复的精确路径**直接抛错
|
|
12
|
+
* (`connection: exact Fetch route "…" is already registered`),撞了就是整棵插件树 boot 失败——
|
|
13
|
+
* 所以这条路径是与生态的硬边界,不是显示好不好看的问题。
|
|
14
14
|
*/
|
|
15
15
|
export declare const SESSION_DELETE_API_PREFIX = "/dsh-session-delete-api";
|
|
16
|
+
/**
|
|
17
|
+
* 唯一的取数基础路径:宿主 `/api` + 插件前缀。九段 exact Fetch 路由、客户端探测与附件 URL
|
|
18
|
+
* 都从它起;`SESSION_DELETE_API_PREFIX` 只作为段名的相对前缀与错误原文。
|
|
19
|
+
*/
|
|
20
|
+
export declare const OFFICIAL_SESSION_DELETE_API_PREFIX = "/api/dsh-session-delete-api";
|
|
16
21
|
/** 九个段名(相对前缀)。**每项 `{ path, method }` 两键**:某段用什么方法也是线上形状的一部分
|
|
17
22
|
* (ADR-0003),家就在这一张表——路由记录(`src/api/route-table.ts`)逐行投影(`ROUTES.x.method`),
|
|
18
23
|
* 浏览器半的 GET 段同样投影、写段把 `ROUTES.x.method` 交给 `src/client/remote.ts` 的 `post()`,
|
|
@@ -251,7 +256,8 @@ export interface AttachmentOrphanItemWire {
|
|
|
251
256
|
bytes: number;
|
|
252
257
|
}
|
|
253
258
|
/** 孤儿附件报告(`GET /attachment-orphans`):数量、总字节与对象清单。
|
|
254
|
-
* 清单只带 id/bytes
|
|
259
|
+
* 清单只带 id/bytes(不含路径/文件名),供设置页预览缩略图;条数由 Node 半配置
|
|
260
|
+
* (`src/config.ts` 的 `orphanPreviewLimit`,默认 200),数量与字节按全量算。
|
|
255
261
|
* 读不出任何一处引用源时**不返回这一形状**,而是 `io` 失败——不拿 0 冒充"没有孤儿"。 */
|
|
256
262
|
export interface AttachmentOrphanReportWire {
|
|
257
263
|
count: number;
|
|
@@ -297,6 +303,13 @@ export declare const PARAMS: {
|
|
|
297
303
|
readonly attachmentId: "id";
|
|
298
304
|
readonly attachmentKind: "kind";
|
|
299
305
|
readonly attachmentName: "name";
|
|
306
|
+
/** 图片引用的媒体类型 / 宽高:只给宿主 `attachments.readImage` 做引用校验,不参与路径拼接。 */
|
|
307
|
+
readonly attachmentMediaType: "mediaType";
|
|
308
|
+
/** 字节数:图片支与文件支都带——它是宿主读面的引用一部分,并先过 `maxAttachmentBytes` 闸
|
|
309
|
+
* (别为一张已知超限的图让宿主整份读出来)。 */
|
|
310
|
+
readonly attachmentBytes: "bytes";
|
|
311
|
+
readonly attachmentWidth: "width";
|
|
312
|
+
readonly attachmentHeight: "height";
|
|
300
313
|
};
|
|
301
314
|
/**
|
|
302
315
|
* 失败信封的**线上形状**(本文件):`code` 是判别依据、`message` 是上屏原文、
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isAbsentPath } from '../ports/fs-port.js';
|
|
1
2
|
import type { FsPort } from '../ports/fs-port.js';
|
|
3
|
+
import type { SessionDeleteConfig } from '../config.js';
|
|
2
4
|
import { type AttachmentCleanupWire } from '../shared/wire.js';
|
|
3
5
|
export interface ImageRef {
|
|
4
6
|
kind: 'image';
|
|
@@ -21,8 +23,10 @@ export type AttachmentRef = ImageRef | FileRef;
|
|
|
21
23
|
* 假支若按「是 string」取反会被 TS 收成 `never`(参数本来就是 string),插值就成了 never 读数。
|
|
22
24
|
* 这里判的仍是同一条正则,谓词只是把「形状」写进类型面。 */
|
|
23
25
|
export declare function isAttachmentId(value: unknown): value is `sha256:${string}`;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
+
/** 路径缺席的判据只有一处:`src/ports/fs-port.ts` 的 `isAbsentPath`(ENOENT / ENOTDIR)。
|
|
27
|
+
* `statFile` / `readFile` / `sizeOfDir` 的读错误里,"不在"只认这两枚;权限、盘挂、I/O 错误
|
|
28
|
+
* 都必须露成读不出(ADR-0039),别在这里另写一份只有 ENOENT 的抄本。 */
|
|
29
|
+
export declare const isMissing: typeof isAbsentPath;
|
|
26
30
|
/** 合法附件 ID → 宿主附件库里的绝对路径。`name` 不参与拼接。 */
|
|
27
31
|
export declare function attachmentPathOf(home: string, kind: 'image' | 'file', id: string): string;
|
|
28
32
|
/** 一条消息的 `content[]` 里的附件块;畸形 id / 非附件段一律跳过。 */
|
|
@@ -30,6 +34,7 @@ export declare function attachmentsInContent(content: unknown): AttachmentRef[];
|
|
|
30
34
|
export interface AttachmentReadDeps {
|
|
31
35
|
fs: Pick<FsPort, 'exists' | 'listFiles' | 'listDirs' | 'readFile' | 'statFile' | 'realpath'>;
|
|
32
36
|
home: string;
|
|
37
|
+
config: Pick<SessionDeleteConfig, 'maxJsonRefBytes'>;
|
|
33
38
|
}
|
|
34
39
|
export interface AttachmentRemovalDeps extends AttachmentReadDeps {
|
|
35
40
|
fs: Pick<FsPort, 'exists' | 'listFiles' | 'listDirs' | 'readFile' | 'statFile' | 'realpath' | 'removeFile'>;
|
|
@@ -65,6 +70,16 @@ export interface ImageRemovalPlan {
|
|
|
65
70
|
* 剩余引用源读不出 ⇒ 整体跳过;候选日志读不出 / 附件对象 stat 失败 ⇒ 只跳过那一部分,其余照常。
|
|
66
71
|
*/
|
|
67
72
|
export declare function planImageAttachmentRemoval(deps: AttachmentReadDeps, removedDirs: readonly string[]): Promise<ImageRemovalPlan>;
|
|
73
|
+
/**
|
|
74
|
+
* 执行期复核:计划与真删对象之间隔着一整趟目录删除(秒级),这期间别的会话可能把同一张图
|
|
75
|
+
* (内容寻址 ⇒ 同一个 sha256 id)再引用一次,或派生存储新写一条引用。计划是数据、会过期;
|
|
76
|
+
* 删对象之前用同一套剩余引用扫描再判一次,**只删这一刻仍无引用的那些**(ADR-0041)。
|
|
77
|
+
*
|
|
78
|
+
* 调用时机:会话 / 回收站目录已经删掉之后、`executeImageAttachmentRemoval` 之前——那时被删的
|
|
79
|
+
* 引用源已经不在了,扫到的就是真正的"剩余"。读不出照旧 fail closed:回空计划 + `skipped`,
|
|
80
|
+
* 宁可留下孤儿也不删共享。空计划直接返回(不为零删付一次全 home 扫描)。
|
|
81
|
+
*/
|
|
82
|
+
export declare function recheckImageRemovalExclusivity(deps: AttachmentReadDeps, plan: ImageRemovalPlan): Promise<ImageRemovalPlan>;
|
|
68
83
|
/**
|
|
69
84
|
* 逐文件删;ENOENT 由端口的 `force` 吞掉;其他错误计数并继续。
|
|
70
85
|
*
|
|
@@ -21,5 +21,7 @@ export declare function routeError(code: RouteCode, httpStatus: number, message:
|
|
|
21
21
|
export declare function rollbackReason(label: string, error: unknown): string;
|
|
22
22
|
/** 原错误保留,回滚失败作为 `details.rollbackFailures` 上线;码仍按原错误的动词码,
|
|
23
23
|
* 没有可继承的就落 io。**内层的回滚失败要并进来**:一个成员自己退失败、兄弟也退失败时,
|
|
24
|
-
* 两份失败都得在 details 里,消息也不能出现第二个「回滚未完成」。
|
|
24
|
+
* 两份失败都得在 details 里,消息也不能出现第二个「回滚未完成」。
|
|
25
|
+
* **原错误 details 的其余键原样保留**:孤儿清理的部分回执(`removed` / `freedBytes` /
|
|
26
|
+
* `failedSessionId`)就挂在它上面,整份替换会把已经发生的删除报成 0。 */
|
|
25
27
|
export declare function withRollbackFailures(error: unknown, failures: readonly string[]): VerbError;
|
|
@@ -27,8 +27,9 @@ interface RawGroup {
|
|
|
27
27
|
* 彻底删除(组语义见 `src/trash/groups.ts`),把它的根当孤儿删掉就是**替用户做了一个没人点过的决定**。孤儿只
|
|
28
28
|
* 有一种来路:父被彻底删掉了(或被手工从盘上清掉了),清册里再也没有这一行。
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
* `src/domain/closure.ts` 的 `orphanRootsOf
|
|
30
|
+
* 根按**传递闭包**收全部 subagent 后代(与 `HostCorpusPort.subagentMembersOf` 同一实现:闭包核心住
|
|
31
|
+
* `src/domain/closure.ts` 的 `orphanRootsOf`,孩子与孙代都算,深度不设上限;普通 fork 不是成员、
|
|
32
|
+
* 也不穿过它收后代),同一条记录只归一组
|
|
32
33
|
* (`claimed`)——数据里真出现环时结果是"少数一条",不是把一个会话删两遍。
|
|
33
34
|
* 归堆与去重**两边都去 `session-` 前缀**:同一个父在两代宿主里写法不同(见 `bareSessionId` 的来历)。
|
|
34
35
|
*/
|
|
@@ -130,15 +130,20 @@ export declare function planOrphanRemoval(deps: CoreDeps, members: readonly {
|
|
|
130
130
|
*
|
|
131
131
|
* 顺序:整批摘归档集 → 摘有账本的成员的账本 → 逐条回收站行
|
|
132
132
|
* (在才删目录 → 摘清册行 → 通告成员离场)→ 逐条盘上成员(删目录 → 通告成员离场)→ 逐条附件对象。
|
|
133
|
-
* 目录删掉就没有"退回去"
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
133
|
+
* 目录删掉就没有"退回去"这回事,中途失败**不回滚目录**;已经删掉的由下面的计数器如实交回。
|
|
134
|
+
* **归档态回滚**:`unarchive` 排在所有删除之前(免得留下"会话没了、归档集还挂着"的幽灵 id),
|
|
135
|
+
* 所以中途失败时目录还在、归档位却已经摘了。失败出口把**目录还在**的那些(这一趟没删掉、也不是计划时
|
|
136
|
+
* 就缺席)按删除前的读数重新归档(`isArchived` 先读过一次,不是按 `wasArchived` 猜);目录不在的跳过
|
|
137
|
+
* ——那时恢复归档只会造幽灵 id。
|
|
138
|
+
* 回滚自己的失败走 `details.rollbackFailures`,不让它把原错误吞掉(ADR-0040)。
|
|
139
|
+
* **附件计划在执行期复核**:计划到删对象之间的窗口里新增的引用会让计划过期;删对象前用同一套
|
|
140
|
+
* 剩余引用扫描重判一次,只删这一刻仍无引用的对象(ADR-0041)。
|
|
137
141
|
*
|
|
138
142
|
* 失败形状:
|
|
139
143
|
* - `kind:'orphan'`(orphan-clean)⇒ `io` + `{removed, freedBytes, failedSessionId}`,不伪装成整组没动;
|
|
140
144
|
* - `kind:'trash'`(purge)⇒ 同一个标签、不带 details;
|
|
141
|
-
* - orphan-clean
|
|
145
|
+
* - orphan-clean 的归档实况预读 / unarchive / detach 原样抛出;
|
|
146
|
+
* - 归档态回滚不干净时,原错误带 `details.rollbackFailures`(码仍按原错误的动词码)。
|
|
142
147
|
*
|
|
143
148
|
* 通告只经 `member-presence`(摘活体三态 + `api-session/removed`),不在这里另写一份。
|
|
144
149
|
*/
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"meta": {
|
|
3
|
-
"title": "Session Delete",
|
|
4
|
-
"description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
|
|
5
|
-
}
|
|
6
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Session Delete",
|
|
4
|
+
"description": "Delete sessions: delete moves them to a recoverable trash, with an explicit permanent delete."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"meta": {
|
|
3
|
-
"title": "会话删除",
|
|
4
|
-
"description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
|
|
5
|
-
}
|
|
6
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "会话删除",
|
|
4
|
+
"description": "删除会话:删除 = 移入回收站(可恢复),另设显式的彻底删除。"
|
|
5
|
+
}
|
|
6
|
+
}
|