@dsh-workbench/shared 0.3.6 → 0.3.8
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/dist/api/context-ref.d.ts +0 -12
- package/dist/api/context-ref.js +0 -40
- package/dist/api/facts.d.ts +0 -6
- package/dist/api/facts.js +0 -8
- package/dist/api/long-task.d.ts +8 -0
- package/dist/api/long-task.js +10 -0
- package/dist/components/ConfirmBar.css +26 -0
- package/dist/components/ConfirmBar.d.ts +36 -0
- package/dist/components/ConfirmBar.js +26 -0
- package/dist/components/Form.d.ts +7 -5
- package/dist/components/Form.js +5 -3
- package/dist/components/FormNodes.d.ts +17 -13
- package/dist/components/FormNodes.js +13 -5
- package/dist/components/FormTypes.d.ts +39 -14
- package/dist/components/MockStrip.css +11 -1
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +2 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/board-fact.d.ts +8 -0
- package/dist/host/board-fact.js +16 -0
- package/dist/host/browser-read.js +2 -1
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/context-ref.d.ts +13 -0
- package/dist/host/context-ref.js +41 -0
- package/dist/host/diy-debug.d.ts +80 -0
- package/dist/host/diy-debug.js +88 -0
- package/dist/host/diy-record.d.ts +135 -0
- package/dist/host/diy-record.js +162 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/facts.d.ts +6 -0
- package/dist/host/facts.js +9 -0
- package/dist/host/git.js +1 -1
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +3 -3
- package/dist/host/long-task.d.ts +5 -0
- package/dist/host/long-task.js +3 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/opencli-runtime.js +1 -1
- package/dist/host/opencli-window.js +1 -1
- package/dist/host/opencli.js +2 -1
- package/dist/host/panel-state.d.ts +120 -0
- package/dist/host/panel-state.js +176 -0
- package/dist/host/plugin-source.js +1 -1
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/shell-text.d.ts +13 -0
- package/dist/host/shell-text.js +20 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-declaration.d.ts +65 -0
- package/dist/host/unit-declaration.js +213 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host.d.ts +7 -4
- package/dist/host.js +7 -4
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/index.d.ts +5 -1
- package/dist/i18n/zh.d.ts +4 -0
- package/dist/i18n/zh.js +4 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/lib/board-fact.d.ts +0 -7
- package/dist/lib/board-fact.js +0 -15
- package/dist/lib/diy-record.d.ts +38 -1
- package/dist/lib/diy-record.js +66 -0
- package/dist/lib/text.d.ts +0 -13
- package/dist/lib/text.js +0 -20
- package/dist/testing/declaration.js +1 -1
- package/package.json +1 -1
- package/src/api/__tests__/facts.test.ts +0 -13
- package/src/api/__tests__/long-task.test.ts +15 -0
- package/src/api/context-ref.ts +0 -39
- package/src/api/facts.ts +0 -9
- package/src/api/long-task.ts +11 -0
- package/src/components/ConfirmBar.css +26 -0
- package/src/components/ConfirmBar.test.tsx +130 -0
- package/src/components/ConfirmBar.tsx +63 -0
- package/src/components/Form.test.tsx +15 -0
- package/src/components/Form.tsx +7 -5
- package/src/components/FormNodes.tsx +50 -26
- package/src/components/FormTypes.test.ts +31 -0
- package/src/components/FormTypes.ts +46 -15
- package/src/components/FormWiring.test.tsx +1 -0
- package/src/config/constants.ts +3 -0
- package/src/host/__tests__/board-fact.test.ts +47 -0
- package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
- package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
- package/src/host/__tests__/facts.test.ts +13 -0
- package/src/host/__tests__/long-task.test.ts +42 -0
- package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
- package/src/host/__tests__/preflight-route.test.ts +33 -0
- package/src/host/__tests__/shell-text.test.ts +44 -0
- package/src/host/board-fact.ts +14 -0
- package/src/host/browser-read.ts +2 -1
- package/src/host/context-ref.ts +40 -0
- package/src/{lib → host}/diy-record.ts +95 -1
- package/src/host/facts.ts +10 -0
- package/src/host/git.ts +1 -1
- package/src/host/http.ts +4 -4
- package/src/host/long-task.ts +9 -0
- package/src/host/opencli-runtime.ts +1 -1
- package/src/host/opencli-window.ts +1 -1
- package/src/host/opencli.ts +2 -1
- package/src/host/plugin-source.ts +1 -1
- package/src/host/shell-text.ts +21 -0
- package/src/{api → host}/unit-declaration.ts +1 -1
- package/src/host.ts +7 -4
- package/src/i18n/en.ts +4 -0
- package/src/i18n/zh.ts +4 -0
- package/src/index.ts +1 -4
- package/src/lib/board-fact.test.ts +1 -33
- package/src/lib/board-fact.ts +0 -13
- package/src/lib/text.test.ts +1 -43
- package/src/lib/text.ts +0 -22
- package/src/testing/declaration.ts +1 -1
- package/dist/lib/lint-probe.d.ts +0 -1
- package/dist/lib/lint-probe.js +0 -6
- /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
- /package/src/{lib → host}/diy-debug.ts +0 -0
- /package/src/{lib → host}/panel-state.ts +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
const tick = () => delay(0);
|
|
3
|
+
export function createFakeShell(replies = []) {
|
|
4
|
+
const calls = [];
|
|
5
|
+
let running = 0;
|
|
6
|
+
let peak = 0;
|
|
7
|
+
const replyAt = (index) => typeof replies === 'function' ? replies(calls[index], index) : (replies[index] ?? {});
|
|
8
|
+
const shell = {
|
|
9
|
+
resolve(request) {
|
|
10
|
+
calls.push(request);
|
|
11
|
+
return request;
|
|
12
|
+
},
|
|
13
|
+
async execute() {
|
|
14
|
+
const index = calls.length - 1;
|
|
15
|
+
const reply = replyAt(index);
|
|
16
|
+
if (reply.failWith !== undefined)
|
|
17
|
+
throw reply.failWith;
|
|
18
|
+
running += 1;
|
|
19
|
+
peak = Math.max(peak, running);
|
|
20
|
+
return {
|
|
21
|
+
async result() {
|
|
22
|
+
await tick();
|
|
23
|
+
running -= 1;
|
|
24
|
+
return {
|
|
25
|
+
exitCode: reply.exitCode ?? 0,
|
|
26
|
+
timedOut: reply.timedOut ?? false,
|
|
27
|
+
aborted: reply.aborted ?? false,
|
|
28
|
+
stdout: { text: reply.stdout ?? '', truncated: false },
|
|
29
|
+
stderr: { text: reply.stderr ?? '', truncated: false },
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
shell,
|
|
37
|
+
calls,
|
|
38
|
+
peak: () => peak,
|
|
39
|
+
commands: () => calls.map((call) => call.command),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function createFakeOpencli(replies = []) {
|
|
43
|
+
const calls = [];
|
|
44
|
+
const opencli = {
|
|
45
|
+
async json(args, options = {}) {
|
|
46
|
+
const call = {
|
|
47
|
+
args,
|
|
48
|
+
...(options.key === undefined ? {} : { key: options.key }),
|
|
49
|
+
...(options.workdir === undefined ? {} : { workdir: options.workdir }),
|
|
50
|
+
};
|
|
51
|
+
calls.push(call);
|
|
52
|
+
const index = calls.length - 1;
|
|
53
|
+
const reply = typeof replies === 'function' ? replies(call, index) : (replies[index] ?? []);
|
|
54
|
+
if (reply instanceof Error)
|
|
55
|
+
throw reply;
|
|
56
|
+
return { value: reply, at: Date.now() };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
opencli,
|
|
61
|
+
calls,
|
|
62
|
+
keys: () => calls.map((call) => call.key),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function createFakeLogger() {
|
|
66
|
+
const warnings = [];
|
|
67
|
+
return {
|
|
68
|
+
logger: {
|
|
69
|
+
info: () => { },
|
|
70
|
+
warn: (message) => {
|
|
71
|
+
warnings.push(message);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createFakeShell } from './fakes';
|
|
2
|
+
export declare const GOOD_BRIDGE_REPORT: string;
|
|
3
|
+
export declare const ALL_OK: {
|
|
4
|
+
stdout: string;
|
|
5
|
+
}[];
|
|
6
|
+
export declare const STALE_DAEMON_BRIDGE: string;
|
|
7
|
+
export declare const BROKEN_BRIDGE: string;
|
|
8
|
+
export declare function build(replies: Parameters<typeof createFakeShell>[0], install?: () => Promise<{
|
|
9
|
+
ok: boolean;
|
|
10
|
+
detail: string;
|
|
11
|
+
}>, warmReads?: () => readonly (readonly string[])[]): {
|
|
12
|
+
fake: {
|
|
13
|
+
shell: import("../..").DshShell;
|
|
14
|
+
calls: import("./fakes").ShellCall[];
|
|
15
|
+
peak: () => number;
|
|
16
|
+
commands: () => string[];
|
|
17
|
+
};
|
|
18
|
+
preflight: import("../preflight").Preflight;
|
|
19
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createPreflight } from '../preflight';
|
|
2
|
+
import { createFakeShell } from './fakes';
|
|
3
|
+
export const GOOD_BRIDGE_REPORT = [
|
|
4
|
+
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
5
|
+
'',
|
|
6
|
+
'[OK] Daemon: running on port 19825 (v1.8.7)',
|
|
7
|
+
'[OK] Extension: connected (v1.0.24)',
|
|
8
|
+
'[OK] Connectivity: connected in 0.0s',
|
|
9
|
+
'',
|
|
10
|
+
'Everything looks good!',
|
|
11
|
+
].join('\n');
|
|
12
|
+
export const ALL_OK = [{ stdout: '1.8.8\n' }, { stdout: GOOD_BRIDGE_REPORT }];
|
|
13
|
+
export const STALE_DAEMON_BRIDGE = [
|
|
14
|
+
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
15
|
+
'',
|
|
16
|
+
'[STALE] Daemon: running on port 19825 (v1.8.8 != CLI v1.8.7)',
|
|
17
|
+
'[OK] Extension: connected (v1.0.24)',
|
|
18
|
+
'[OK] Connectivity: connected in 0.0s',
|
|
19
|
+
'',
|
|
20
|
+
'Issues:',
|
|
21
|
+
' • Stale daemon detected: daemon v1.8.8 != CLI v1.8.7.',
|
|
22
|
+
' Run: opencli daemon restart',
|
|
23
|
+
].join('\n');
|
|
24
|
+
export const BROKEN_BRIDGE = [
|
|
25
|
+
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
26
|
+
'',
|
|
27
|
+
'[MISSING] Daemon: not running',
|
|
28
|
+
'[MISSING] Extension: not connected',
|
|
29
|
+
'[FAIL] Connectivity: failed (daemon not reachable)',
|
|
30
|
+
'',
|
|
31
|
+
'Issues:',
|
|
32
|
+
" • Daemon is not running. It should start automatically when you run an opencli browser command.",
|
|
33
|
+
' • Daemon is running but the Chrome/Chromium extension is not connected.',
|
|
34
|
+
'If the extension is not installed:',
|
|
35
|
+
' 1. Download from https://github.com/jackwener/opencli/releases',
|
|
36
|
+
' 2. Open chrome://extensions/ → Enable Developer Mode',
|
|
37
|
+
' 3. Click "Load unpacked" → select the extension folder',
|
|
38
|
+
].join('\n');
|
|
39
|
+
export function build(replies, install, warmReads) {
|
|
40
|
+
const fake = createFakeShell(replies);
|
|
41
|
+
const preflight = createPreflight({
|
|
42
|
+
shell: fake.shell,
|
|
43
|
+
...(install === undefined ? {} : { install }),
|
|
44
|
+
...(warmReads === undefined ? {} : { warmReads }),
|
|
45
|
+
});
|
|
46
|
+
return { fake, preflight };
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BoardFactState, type BoardProbe } from '../lib/board-fact.js';
|
|
2
|
+
/**
|
|
3
|
+
* 把一次探针读数归成椅面归属。
|
|
4
|
+
* @param probe - 探针读数。
|
|
5
|
+
* @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
|
|
6
|
+
* @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
|
|
7
|
+
*/
|
|
8
|
+
export declare function boardFactOf(probe: BoardProbe, workshop: string | null): BoardFactState;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { insideWorkshop } from '../lib/board-fact.js';
|
|
2
|
+
/**
|
|
3
|
+
* 把一次探针读数归成椅面归属。
|
|
4
|
+
* @param probe - 探针读数。
|
|
5
|
+
* @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
|
|
6
|
+
* @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
|
|
7
|
+
*/
|
|
8
|
+
export function boardFactOf(probe, workshop) {
|
|
9
|
+
if (!probe.workbench)
|
|
10
|
+
return 'unknown';
|
|
11
|
+
if (!probe.bar || !probe.open || !probe.drawn)
|
|
12
|
+
return 'hidden';
|
|
13
|
+
if (probe.board === '' || workshop === null)
|
|
14
|
+
return 'unknown';
|
|
15
|
+
return insideWorkshop(workshop, probe.board) ? 'workshop' : 'other';
|
|
16
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { parseJsonLoose
|
|
1
|
+
import { parseJsonLoose } from '../lib/text.js';
|
|
2
|
+
import { tailLines } from './shell-text.js';
|
|
2
3
|
import { backgroundLine } from './opencli-window.js';
|
|
3
4
|
import { runShell } from './shell.js';
|
|
4
5
|
export const BROWSER_SCREEN_SESSION = 'workbench';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ContextRef } from '@dsh-workbench/shared';
|
|
2
|
+
import type { ContextRenderer } from './unit-face';
|
|
3
|
+
export declare const CONTEXT_SOURCE_KIND = "workbench";
|
|
4
|
+
export interface ScannableMessage {
|
|
5
|
+
readonly source?: {
|
|
6
|
+
readonly kind?: string;
|
|
7
|
+
};
|
|
8
|
+
readonly content?: readonly {
|
|
9
|
+
readonly type?: string;
|
|
10
|
+
readonly text?: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
export type { ContextRenderer };
|
|
14
|
+
export declare function collectContextRefs(messages: readonly ScannableMessage[]): ContextRef[];
|
|
15
|
+
export declare function buildContextMessage(text: string, summary: string): unknown;
|
|
16
|
+
export interface ContextInjector {
|
|
17
|
+
inject(messages: readonly ScannableMessage[], agent: unknown): Promise<unknown | undefined>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createContextInjector(options: {
|
|
20
|
+
renderers: ReadonlyMap<string, ContextRenderer>;
|
|
21
|
+
logger?: {
|
|
22
|
+
warn?(message: string): void;
|
|
23
|
+
} | undefined;
|
|
24
|
+
}): ContextInjector;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { encodeRef, parseMarkers } from '@dsh-workbench/shared';
|
|
3
|
+
export const CONTEXT_SOURCE_KIND = 'workbench';
|
|
4
|
+
export function collectContextRefs(messages) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const refs = [];
|
|
7
|
+
for (const message of messages) {
|
|
8
|
+
if (message.source?.kind !== 'user')
|
|
9
|
+
continue;
|
|
10
|
+
for (const block of message.content ?? []) {
|
|
11
|
+
if (block.type !== 'text' || typeof block.text !== 'string')
|
|
12
|
+
continue;
|
|
13
|
+
for (const ref of parseMarkers(block.text)) {
|
|
14
|
+
const key = encodeRef(ref);
|
|
15
|
+
if (seen.has(key))
|
|
16
|
+
continue;
|
|
17
|
+
seen.add(key);
|
|
18
|
+
refs.push(ref);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return refs;
|
|
23
|
+
}
|
|
24
|
+
export function buildContextMessage(text, summary) {
|
|
25
|
+
return {
|
|
26
|
+
role: 'user',
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
content: [{ type: 'text', text }],
|
|
29
|
+
source: { kind: CONTEXT_SOURCE_KIND, form: 'notice', summary },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createContextInjector(options) {
|
|
33
|
+
const done = new WeakMap();
|
|
34
|
+
return {
|
|
35
|
+
async inject(messages, agent) {
|
|
36
|
+
const refs = collectContextRefs(messages);
|
|
37
|
+
if (refs.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
const key = typeof agent === 'object' && agent !== null ? agent : undefined;
|
|
40
|
+
const seen = key === undefined ? undefined : (done.get(key) ?? new Set());
|
|
41
|
+
if (key !== undefined && seen !== undefined)
|
|
42
|
+
done.set(key, seen);
|
|
43
|
+
const fresh = [];
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
const encoded = encodeRef(ref);
|
|
46
|
+
if (seen?.has(encoded) === true)
|
|
47
|
+
continue;
|
|
48
|
+
seen?.add(encoded);
|
|
49
|
+
fresh.push(ref);
|
|
50
|
+
}
|
|
51
|
+
if (fresh.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const texts = [];
|
|
54
|
+
for (const ref of fresh) {
|
|
55
|
+
const render = options.renderers.get(ref.kind);
|
|
56
|
+
if (render === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
let text;
|
|
59
|
+
try {
|
|
60
|
+
text = await render(ref);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
options.logger?.warn?.(`[dsh-workbench] 渲染对话引用失败:${String(error)}`);
|
|
64
|
+
text = `工作台面板引用了一条内容(${encodeRef(ref)}),但当前渲染不出来(项目未选中或数据源不可用)。`;
|
|
65
|
+
}
|
|
66
|
+
if (text !== '')
|
|
67
|
+
texts.push(text);
|
|
68
|
+
}
|
|
69
|
+
if (texts.length === 0)
|
|
70
|
+
return undefined;
|
|
71
|
+
return buildContextMessage(texts.join('\n\n'), `工作台引用:${fresh.length} 条`);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ContextRef } from '../api/context-ref.js';
|
|
2
|
+
/**
|
|
3
|
+
* 把 kind:part/part 文本读回引用。
|
|
4
|
+
* @param text 待解析文本。
|
|
5
|
+
* @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
|
|
6
|
+
*/
|
|
7
|
+
export declare function decodeRef(text: string): ContextRef | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
|
|
10
|
+
* @param text 任意文本;null / undefined 当空串。
|
|
11
|
+
* @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseMarkers(text: string): ContextRef[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CONTEXT_MARKER_NAME, encodeRef } from '../api/context-ref.js';
|
|
2
|
+
const MARKER = new RegExp(`@${CONTEXT_MARKER_NAME}:([a-z][a-z0-9-]*:[A-Za-z0-9._~%#/-]+)`, 'g');
|
|
3
|
+
const TRAILING_PUNCTUATION = /[.,;:!?]+$/;
|
|
4
|
+
/**
|
|
5
|
+
* 把 kind:part/part 文本读回引用。
|
|
6
|
+
* @param text 待解析文本。
|
|
7
|
+
* @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
|
|
8
|
+
*/
|
|
9
|
+
export function decodeRef(text) {
|
|
10
|
+
const at = text.indexOf(':');
|
|
11
|
+
if (at <= 0 || at === text.length - 1)
|
|
12
|
+
return undefined;
|
|
13
|
+
const kind = text.slice(0, at);
|
|
14
|
+
if (!/^[a-z][a-z0-9-]*$/.test(kind))
|
|
15
|
+
return undefined;
|
|
16
|
+
const parts = text.slice(at + 1).split('/').filter((part) => part !== '');
|
|
17
|
+
if (parts.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return { kind, parts };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
|
|
23
|
+
* @param text 任意文本;null / undefined 当空串。
|
|
24
|
+
* @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
|
|
25
|
+
*/
|
|
26
|
+
export function parseMarkers(text) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const refs = [];
|
|
29
|
+
for (const match of String(text ?? '').matchAll(MARKER)) {
|
|
30
|
+
const raw = match[1].replace(TRAILING_PUNCTUATION, '');
|
|
31
|
+
const ref = decodeRef(raw);
|
|
32
|
+
if (ref === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
const key = encodeRef(ref);
|
|
35
|
+
if (seen.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
refs.push(ref);
|
|
39
|
+
}
|
|
40
|
+
return refs;
|
|
41
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 调试约定的两个相位:`debugging` 正占着用户的屏,`paused` 已暂停但仍持着约定。
|
|
3
|
+
*/
|
|
4
|
+
export type DebugPhase = 'debugging' | 'paused';
|
|
5
|
+
/**
|
|
6
|
+
* 调试约定收得下的三个人话动作:暂停 / 继续 / 结束。
|
|
7
|
+
*/
|
|
8
|
+
export type DebugIntent = 'pause' | 'resume' | 'stop';
|
|
9
|
+
/**
|
|
10
|
+
* 把工具入参收成人话动作。
|
|
11
|
+
* @param value - 任意入参值。
|
|
12
|
+
* @returns `'pause'` / `'resume'` / `'stop'` 原样;别的值(含 `undefined`、多余空白、大小写不同)给 `null`,由调用方拒。
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseDebugIntent(value: unknown): DebugIntent | null;
|
|
15
|
+
/**
|
|
16
|
+
* 调试约定的有效期(4 小时,毫秒):超过它没动静的约定算过期、当场让人。
|
|
17
|
+
*/
|
|
18
|
+
export declare const DEBUG_STALE_MS: number;
|
|
19
|
+
/**
|
|
20
|
+
* 一份活着的调试约定:相位 + 谁的会话 + 哪个工作区 / 板 / 单元 + 起于何时(`at` 是最后一次动作)。
|
|
21
|
+
*/
|
|
22
|
+
export interface DebugState {
|
|
23
|
+
readonly phase: DebugPhase;
|
|
24
|
+
readonly sessionId: string;
|
|
25
|
+
readonly workspacePath: string;
|
|
26
|
+
readonly title: string;
|
|
27
|
+
readonly board: string;
|
|
28
|
+
readonly unit: string;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly startedAt: number;
|
|
31
|
+
readonly at: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 建立约定要交的那几件:会话、工作区、板、单元、名字,以及此刻的时间戳。
|
|
35
|
+
*/
|
|
36
|
+
export interface DebugPact {
|
|
37
|
+
readonly sessionId: string;
|
|
38
|
+
readonly workspacePath: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly board: string;
|
|
41
|
+
readonly unit: string;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly now: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 约定被拒的原因码:自己已持有 / 别人持着 / 不是持有者 / 没有约定 / 已暂停 / 已在调试。
|
|
47
|
+
*/
|
|
48
|
+
export type DebugRefusal = 'already-held' | 'held-by-other' | 'not-owner' | 'none' | 'already-paused' | 'already-debugging';
|
|
49
|
+
/**
|
|
50
|
+
* 一次约定动作的结果:成功带新状态,失败带拒绝码。
|
|
51
|
+
*/
|
|
52
|
+
export type DebugMove = {
|
|
53
|
+
readonly ok: true;
|
|
54
|
+
readonly state: DebugState;
|
|
55
|
+
} | {
|
|
56
|
+
readonly ok: false;
|
|
57
|
+
readonly code: DebugRefusal;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* 调试约定的存取口(外壳持有一份):`live` 取当前有效的约定,`begin` / `pause` / `resume` / `stop` 推相位。
|
|
61
|
+
*/
|
|
62
|
+
export interface DebugStore {
|
|
63
|
+
live(now: number): DebugState | null;
|
|
64
|
+
begin(pact: DebugPact): DebugMove;
|
|
65
|
+
pause(sessionId: string, now: number): DebugMove;
|
|
66
|
+
resume(sessionId: string, now: number): DebugMove;
|
|
67
|
+
stop(sessionId: string, now: number): DebugMove;
|
|
68
|
+
settle?(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 造一份新的调试约定存储(进程内存,同一时刻只可能有一份有效约定)。
|
|
72
|
+
* @returns 存取口;动作成不成都在返回的 `DebugMove` 里,不抛异常。
|
|
73
|
+
*/
|
|
74
|
+
export declare function createDebugStore(): DebugStore;
|
|
75
|
+
/**
|
|
76
|
+
* 「谁还占着」的一句话,用于别人来抢时说清现况。
|
|
77
|
+
* @param state - 当前有效的那份约定。
|
|
78
|
+
* @returns 形如「会话 x(自 HH:mm:ssZ 起在调试 y)」的中文串。
|
|
79
|
+
*/
|
|
80
|
+
export declare function debugOwnerText(state: DebugState): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把工具入参收成人话动作。
|
|
3
|
+
* @param value - 任意入参值。
|
|
4
|
+
* @returns `'pause'` / `'resume'` / `'stop'` 原样;别的值(含 `undefined`、多余空白、大小写不同)给 `null`,由调用方拒。
|
|
5
|
+
*/
|
|
6
|
+
export function parseDebugIntent(value) {
|
|
7
|
+
return value === 'pause' || value === 'resume' || value === 'stop' ? value : null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 调试约定的有效期(4 小时,毫秒):超过它没动静的约定算过期、当场让人。
|
|
11
|
+
*/
|
|
12
|
+
export const DEBUG_STALE_MS = 4 * 60 * 60 * 1000;
|
|
13
|
+
function alive(state, now) {
|
|
14
|
+
if (state === null)
|
|
15
|
+
return null;
|
|
16
|
+
return now - state.at <= DEBUG_STALE_MS ? state : null;
|
|
17
|
+
}
|
|
18
|
+
function ownerOf(state, sessionId, now) {
|
|
19
|
+
const held = alive(state, now);
|
|
20
|
+
if (held === null)
|
|
21
|
+
return 'none';
|
|
22
|
+
return held.sessionId === sessionId ? held : 'not-owner';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 造一份新的调试约定存储(进程内存,同一时刻只可能有一份有效约定)。
|
|
26
|
+
* @returns 存取口;动作成不成都在返回的 `DebugMove` 里,不抛异常。
|
|
27
|
+
*/
|
|
28
|
+
export function createDebugStore() {
|
|
29
|
+
let held = null;
|
|
30
|
+
const live = (now) => alive(held, now);
|
|
31
|
+
return {
|
|
32
|
+
live,
|
|
33
|
+
begin(pact) {
|
|
34
|
+
const current = alive(held, pact.now);
|
|
35
|
+
if (current !== null) {
|
|
36
|
+
return { ok: false, code: current.sessionId === pact.sessionId ? 'already-held' : 'held-by-other' };
|
|
37
|
+
}
|
|
38
|
+
const state = {
|
|
39
|
+
phase: 'debugging',
|
|
40
|
+
sessionId: pact.sessionId,
|
|
41
|
+
workspacePath: pact.workspacePath,
|
|
42
|
+
title: pact.title,
|
|
43
|
+
board: pact.board,
|
|
44
|
+
unit: pact.unit,
|
|
45
|
+
name: pact.name,
|
|
46
|
+
startedAt: pact.now,
|
|
47
|
+
at: pact.now,
|
|
48
|
+
};
|
|
49
|
+
held = state;
|
|
50
|
+
return { ok: true, state };
|
|
51
|
+
},
|
|
52
|
+
pause(sessionId, now) {
|
|
53
|
+
const found = ownerOf(held, sessionId, now);
|
|
54
|
+
if (typeof found === 'string')
|
|
55
|
+
return { ok: false, code: found };
|
|
56
|
+
if (found.phase === 'paused')
|
|
57
|
+
return { ok: false, code: 'already-paused' };
|
|
58
|
+
const state = { ...found, phase: 'paused', at: now };
|
|
59
|
+
held = state;
|
|
60
|
+
return { ok: true, state };
|
|
61
|
+
},
|
|
62
|
+
resume(sessionId, now) {
|
|
63
|
+
const found = ownerOf(held, sessionId, now);
|
|
64
|
+
if (typeof found === 'string')
|
|
65
|
+
return { ok: false, code: found };
|
|
66
|
+
if (found.phase === 'debugging')
|
|
67
|
+
return { ok: false, code: 'already-debugging' };
|
|
68
|
+
const state = { ...found, phase: 'debugging', at: now };
|
|
69
|
+
held = state;
|
|
70
|
+
return { ok: true, state };
|
|
71
|
+
},
|
|
72
|
+
stop(sessionId, now) {
|
|
73
|
+
const found = ownerOf(held, sessionId, now);
|
|
74
|
+
if (typeof found === 'string')
|
|
75
|
+
return { ok: false, code: found };
|
|
76
|
+
held = null;
|
|
77
|
+
return { ok: true, state: found };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 「谁还占着」的一句话,用于别人来抢时说清现况。
|
|
83
|
+
* @param state - 当前有效的那份约定。
|
|
84
|
+
* @returns 形如「会话 x(自 HH:mm:ssZ 起在调试 y)」的中文串。
|
|
85
|
+
*/
|
|
86
|
+
export function debugOwnerText(state) {
|
|
87
|
+
return `会话 ${state.sessionId}(自 ${new Date(state.startedAt).toISOString().slice(11, 19)}Z 起在调试 ${state.unit})`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 录制交接的三个相位:`armed` 等用户在外站页面上动手,`recording` 正在录,`finished` 录完待取。
|
|
3
|
+
*/
|
|
4
|
+
export type RecordPhase = 'armed' | 'recording' | 'finished';
|
|
5
|
+
/**
|
|
6
|
+
* 录制约定的有效期(15 分钟,毫秒):超过它没动静的约定算过期、当场让人。
|
|
7
|
+
*/
|
|
8
|
+
export declare const RECORD_STALE_MS: number;
|
|
9
|
+
/**
|
|
10
|
+
* 这次录制要找的那一次写:HTTP 方法 + URL 里必须含的一段(不填就是只认方法)。
|
|
11
|
+
*/
|
|
12
|
+
export interface RecordExpect {
|
|
13
|
+
readonly method: string;
|
|
14
|
+
readonly urlContains: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 一次「截图」在宿主台账里的那一行:页面上的计数、落图那一刻、落盘路径;没落下来时带 `error` 原文(落成的那条没有它)。
|
|
18
|
+
*/
|
|
19
|
+
export interface RecordShot {
|
|
20
|
+
readonly n: number;
|
|
21
|
+
readonly at: number;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly error?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 某个页面计数已经试过几次:初拍算一次,失败再试一次;计数一换就从头数。
|
|
27
|
+
*/
|
|
28
|
+
export interface RecordShotTries {
|
|
29
|
+
readonly n: number;
|
|
30
|
+
readonly tries: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 一张图最多试几次(初拍 + 一次重试):失败如实报过就停,不每拍重试到底。
|
|
34
|
+
*/
|
|
35
|
+
export declare const RECORD_SHOT_TRIES = 2;
|
|
36
|
+
/**
|
|
37
|
+
* 一份活着的录制约定:相位 + 谁的会话 + 哪个工作区 / 单元 + 页面 URL 与标题 + 要找的那一次写 +
|
|
38
|
+
* 落盘文件 + 三个时间戳,以及两份台账:按计数落图的 `shots`(`shotsSeen` 只记落成的那个计数)
|
|
39
|
+
* 与门铃的 `beats` / `beatMs`(命中几次、最近一次等了多久)。
|
|
40
|
+
*/
|
|
41
|
+
export interface RecordState {
|
|
42
|
+
readonly phase: RecordPhase;
|
|
43
|
+
readonly sessionId: string;
|
|
44
|
+
readonly workspace: string;
|
|
45
|
+
readonly unit: string;
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly url: string;
|
|
48
|
+
readonly title: string;
|
|
49
|
+
readonly action: string;
|
|
50
|
+
readonly expect: RecordExpect | null;
|
|
51
|
+
readonly captureFile: string;
|
|
52
|
+
readonly startedAt: number;
|
|
53
|
+
readonly t0: number | null;
|
|
54
|
+
readonly t1: number | null;
|
|
55
|
+
readonly shots: readonly RecordShot[];
|
|
56
|
+
readonly shotsSeen: number;
|
|
57
|
+
readonly shotTries: RecordShotTries | null;
|
|
58
|
+
readonly beats: number;
|
|
59
|
+
readonly beatMs: number;
|
|
60
|
+
readonly at: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 开一次录制要交的那几件:会话、工作区、单元、名字、页面 URL 与标题、动作名、期望、落盘文件,以及此刻时间。
|
|
64
|
+
*/
|
|
65
|
+
export interface RecordPact {
|
|
66
|
+
readonly sessionId: string;
|
|
67
|
+
readonly workspace: string;
|
|
68
|
+
readonly unit: string;
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly url: string;
|
|
71
|
+
readonly title: string;
|
|
72
|
+
readonly action: string;
|
|
73
|
+
readonly expect: RecordExpect | null;
|
|
74
|
+
readonly captureFile: string;
|
|
75
|
+
readonly now: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 录制被拒的原因码:自己已持有 / 别人持着 / 不是持有者 / 没有约定 / 已在录 / 还没 arm / 不在录 / 还没录完。
|
|
79
|
+
*/
|
|
80
|
+
export type RecordRefusal = 'already-held' | 'held-by-other' | 'not-owner' | 'none' | 'already-recording' | 'not-armed' | 'not-recording' | 'not-finished';
|
|
81
|
+
/**
|
|
82
|
+
* 一次录制动作的结果:成功带新状态,失败带拒绝码。
|
|
83
|
+
*/
|
|
84
|
+
export type RecordMove = {
|
|
85
|
+
readonly ok: true;
|
|
86
|
+
readonly state: RecordState;
|
|
87
|
+
} | {
|
|
88
|
+
readonly ok: false;
|
|
89
|
+
readonly code: RecordRefusal;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 录制约定收得下的三个人话动作:看看进度 / 停手 / 结束并取回。
|
|
93
|
+
*/
|
|
94
|
+
export type RecordIntent = 'peek' | 'halt' | 'stop';
|
|
95
|
+
/**
|
|
96
|
+
* 把工具入参收成人话动作。
|
|
97
|
+
* @param raw - 任意入参值。
|
|
98
|
+
* @returns `'peek'` / `'halt'` / `'stop'` 原样;别的值(含 `undefined`、多余空白)给 `null`,由调用方拒。
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseRecordIntent(raw: unknown): RecordIntent | null;
|
|
101
|
+
/**
|
|
102
|
+
* 录制约定的存取口(外壳持有一份):相位随 `begin → started → finished` 推进,`release` 是收尾时放掉浏览器那一跳。
|
|
103
|
+
*/
|
|
104
|
+
export interface RecordStore {
|
|
105
|
+
readonly release?: ((state: RecordState) => Promise<{
|
|
106
|
+
readonly tab: boolean;
|
|
107
|
+
readonly capture: boolean;
|
|
108
|
+
}>) | undefined;
|
|
109
|
+
live(now: number): RecordState | null;
|
|
110
|
+
begin(pact: RecordPact): RecordMove;
|
|
111
|
+
started(sessionId: string, at: number, now: number): RecordMove;
|
|
112
|
+
finished(sessionId: string, at: number, now: number): RecordMove;
|
|
113
|
+
shotTaken(sessionId: string, shot: RecordShot, now: number): RecordMove;
|
|
114
|
+
shotFailed(sessionId: string, shot: RecordShot, now: number): RecordMove;
|
|
115
|
+
beat(sessionId: string, ms: number, now: number): RecordMove;
|
|
116
|
+
stop(sessionId: string, now: number): RecordMove;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 这一拍该不该落图:页面计数涨过 `shotsSeen` 才算;同一个计数已经试满就先放下,等他自己再按。
|
|
120
|
+
* @param state - 当前有效的那份约定。
|
|
121
|
+
* @param pageShots - 页面浮条上那个「已拍」计数。
|
|
122
|
+
* @returns 该落的那个计数;不该落给 `null`(没涨 / 试满了 / 不是个正经数)。
|
|
123
|
+
*/
|
|
124
|
+
export declare function shotDueOf(state: RecordState, pageShots: number): number | null;
|
|
125
|
+
/**
|
|
126
|
+
* 造一份新的录制约定存储(进程内存,同一时刻只可能有一份有效约定)。
|
|
127
|
+
* @returns 存取口;动作成不成都在返回的 `RecordMove` 里,不抛异常。
|
|
128
|
+
*/
|
|
129
|
+
export declare function createRecordStore(): RecordStore;
|
|
130
|
+
/**
|
|
131
|
+
* 「谁还占着、在录哪个动作」的一句话,用于别人来抢时说清现况。
|
|
132
|
+
* @param state - 当前有效的那份约定。
|
|
133
|
+
* @returns 形如「会话 x(自 HH:mm:ssZ 起在录 y 的「动作」)」的中文串。
|
|
134
|
+
*/
|
|
135
|
+
export declare function recordOwnerText(state: RecordState): string;
|