@lmzhen/dsh-evolution-skill-history 0.26.26 → 0.27.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/lib/client.js +216 -18
- package/lib/index.js +109 -168
- package/lib/types/client/Panel.d.ts +1 -6
- package/lib/types/client/api.d.ts +9 -10
- package/lib/types/client/contracts.generated.d.ts +37 -0
- package/lib/types/client/fallbacks.generated.d.ts +8 -0
- package/lib/types/client/route-client.generated.d.ts +45 -0
- package/lib/types/client/routes.generated.d.ts +31 -0
- package/lib/types/client/states.generated.d.ts +43 -0
- package/lib/types/index.d.ts +10 -11
- package/lib/types/routes.d.ts +12 -5
- package/package.json +13 -11
package/lib/client.js
CHANGED
|
@@ -5,25 +5,146 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
//#region src/client/routes.generated.ts
|
|
9
|
+
/**
|
|
10
|
+
* Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
|
|
11
|
+
* A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
|
|
12
|
+
* external), so the table is mirrored here mechanically; a rename in core regenerates all three
|
|
13
|
+
* client copies, and the generator's --check makes a stale copy red.
|
|
14
|
+
*/
|
|
15
|
+
const ROUTES = {
|
|
16
|
+
approval: {
|
|
17
|
+
pending: "/api/dsh-evolution/approval/pending",
|
|
18
|
+
preview: "/api/dsh-evolution/approval/preview",
|
|
19
|
+
approve: "/api/dsh-evolution/approval/approve",
|
|
20
|
+
reject: "/api/dsh-evolution/approval/reject"
|
|
21
|
+
},
|
|
22
|
+
"skill-history": {
|
|
23
|
+
skills: "/api/dsh-evolution/skill-history/skills",
|
|
24
|
+
versions: "/api/dsh-evolution/skill-history/versions",
|
|
25
|
+
diff: "/api/dsh-evolution/skill-history/versions/diff",
|
|
26
|
+
body: "/api/dsh-evolution/skill-history/versions/body",
|
|
27
|
+
undo: "/api/dsh-evolution/skill-history/undo",
|
|
28
|
+
health: "/api/dsh-evolution/skill-history/health"
|
|
29
|
+
},
|
|
30
|
+
"skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
|
|
31
|
+
diagnostics: { view: "/api/dsh-evolution/diagnostics" }
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/client/route-client.generated.ts
|
|
35
|
+
/** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
|
|
36
|
+
function classify(status) {
|
|
37
|
+
if (status === 401) return {
|
|
38
|
+
kind: "unauthenticated",
|
|
39
|
+
status: 401
|
|
40
|
+
};
|
|
41
|
+
if (status === 403) return {
|
|
42
|
+
kind: "forbidden",
|
|
43
|
+
status: 403
|
|
44
|
+
};
|
|
45
|
+
if (status === 404) return {
|
|
46
|
+
kind: "unmounted",
|
|
47
|
+
status: 404
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
kind: "server",
|
|
51
|
+
status
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One request, no retries and no body parsing for the platform's admission answers.
|
|
56
|
+
* @param doFetch - the fetch in use (the browser's, or a spec's stub).
|
|
57
|
+
* @param path - the exact route path from the generated ROUTES table.
|
|
58
|
+
* @param init - standard RequestInit.
|
|
59
|
+
* @returns the data, or the classified failure (network/parse failures read as 'server').
|
|
60
|
+
*/
|
|
61
|
+
async function requestJson(doFetch, path, init) {
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await doFetch(path, init);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
failure: {
|
|
69
|
+
kind: "server",
|
|
70
|
+
status: 0,
|
|
71
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) return {
|
|
76
|
+
ok: false,
|
|
77
|
+
failure: classify(response.status)
|
|
78
|
+
};
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = await response.json();
|
|
82
|
+
} catch {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
failure: {
|
|
86
|
+
kind: "server",
|
|
87
|
+
status: response.status
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (typeof body === "object" && body !== null) {
|
|
92
|
+
const record = body;
|
|
93
|
+
if (record.ok === true) return {
|
|
94
|
+
ok: true,
|
|
95
|
+
data: record.data
|
|
96
|
+
};
|
|
97
|
+
if (record.ok === false && typeof record.message === "string" && record.message !== "") return {
|
|
98
|
+
ok: false,
|
|
99
|
+
failure: {
|
|
100
|
+
kind: "refused",
|
|
101
|
+
code: typeof record.code === "string" && record.code !== "" ? record.code : "refused",
|
|
102
|
+
message: record.message
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
failure: {
|
|
109
|
+
kind: "server",
|
|
110
|
+
status: response.status
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The one-sentence rendering legacy callers attach to a classified failure. */
|
|
115
|
+
function failureMessage(failure) {
|
|
116
|
+
switch (failure.kind) {
|
|
117
|
+
case "refused": return failure.message;
|
|
118
|
+
case "server": return failure.status === 0 ? failure.detail === void 0 || failure.detail === "" ? "网络错误:无法连接宿主" : failure.detail : "HTTP " + String(failure.status);
|
|
119
|
+
case "unauthenticated": return "HTTP 401";
|
|
120
|
+
case "forbidden": return "HTTP 403";
|
|
121
|
+
case "unmounted": return "HTTP 404";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
8
125
|
//#region src/client/api.ts
|
|
9
126
|
/**
|
|
10
127
|
* The host routes this bundle calls, and the ONE place a request is made.
|
|
11
128
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
129
|
+
* G5/W7: the paths come from the generated client route table (rendered from evolution-core
|
|
130
|
+
* HOST_ROUTES), and the transport classifier is the shared generated route-client — the browser half
|
|
131
|
+
* can no longer drift from the host table or grow a second HTTP vocabulary.
|
|
15
132
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
16
133
|
*/
|
|
17
|
-
/** Route paths
|
|
18
|
-
const HOST_ROUTES =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
134
|
+
/** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
|
|
135
|
+
const HOST_ROUTES = ROUTES["skill-history"];
|
|
136
|
+
/**
|
|
137
|
+
* A refusal the host reported, carrying its own sentence (the curator's, not ours) and the classified
|
|
138
|
+
* four-state failure so the panel can render the shared failure block (G5/W7 S6b).
|
|
139
|
+
*/
|
|
140
|
+
var SkillHistoryRefusal = class extends Error {
|
|
141
|
+
failure;
|
|
142
|
+
constructor(failure) {
|
|
143
|
+
super(failureMessage(failure));
|
|
144
|
+
this.name = "SkillHistoryRefusal";
|
|
145
|
+
this.failure = failure;
|
|
146
|
+
}
|
|
24
147
|
};
|
|
25
|
-
/** A refusal the host reported, carrying its own sentence (the curator's, not ours). */
|
|
26
|
-
var SkillHistoryRefusal = class extends Error {};
|
|
27
148
|
/**
|
|
28
149
|
* Build the panel's face over one fetch implementation.
|
|
29
150
|
* @param fetchImpl - the fetch to use; a spec passes a stub.
|
|
@@ -31,11 +152,9 @@ window.__ModuleLoader__.load({
|
|
|
31
152
|
*/
|
|
32
153
|
function createSkillHistoryApi(fetchImpl = fetch) {
|
|
33
154
|
const request = async (path, init) => {
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
if (body.ok !== true) throw new SkillHistoryRefusal(body.message ?? "request refused");
|
|
38
|
-
return body.data;
|
|
155
|
+
const answer = await requestJson(fetchImpl, path, init);
|
|
156
|
+
if (!answer.ok) throw new SkillHistoryRefusal(answer.failure);
|
|
157
|
+
return answer.data;
|
|
39
158
|
};
|
|
40
159
|
return {
|
|
41
160
|
skills: async () => await request(HOST_ROUTES.skills),
|
|
@@ -83,6 +202,11 @@ window.__ModuleLoader__.load({
|
|
|
83
202
|
loading: "读取中…",
|
|
84
203
|
refresh: "刷新",
|
|
85
204
|
undo: "回退到此版",
|
|
205
|
+
"failure.unauthorized": "需要登录:重新打开这个页面完成登录后再试。",
|
|
206
|
+
"failure.forbidden": "平台拒绝了这个请求(身份不是 owner)。",
|
|
207
|
+
"failure.unmounted": "该功能没有挂载。",
|
|
208
|
+
"failure.server": "读取失败,可以重试。",
|
|
209
|
+
"fallback.hint": "可以改用命令:{command}",
|
|
86
210
|
"undo.confirm": "确认回退",
|
|
87
211
|
"undo.no": "取消",
|
|
88
212
|
current: "当前",
|
|
@@ -158,6 +282,11 @@ window.__ModuleLoader__.load({
|
|
|
158
282
|
loading: "Loading…",
|
|
159
283
|
refresh: "Refresh",
|
|
160
284
|
undo: "Restore this version",
|
|
285
|
+
"failure.unauthorized": "Sign-in is required: reopen this page and sign in, then retry.",
|
|
286
|
+
"failure.forbidden": "The platform refused this request (not the owner principal).",
|
|
287
|
+
"failure.unmounted": "This feature is not mounted.",
|
|
288
|
+
"failure.server": "The read failed; you can retry.",
|
|
289
|
+
"fallback.hint": "Command fallback: {command}",
|
|
161
290
|
"undo.confirm": "Confirm restore",
|
|
162
291
|
"undo.no": "Cancel",
|
|
163
292
|
current: "Current",
|
|
@@ -848,6 +977,51 @@ window.__ModuleLoader__.load({
|
|
|
848
977
|
] }), toggle), diffView(face, diff.diff, mode));
|
|
849
978
|
}
|
|
850
979
|
//#endregion
|
|
980
|
+
//#region src/client/states.generated.ts
|
|
981
|
+
/** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
|
|
982
|
+
function Block(props) {
|
|
983
|
+
const tone = props.tone ?? "neutral";
|
|
984
|
+
return (0, react.createElement)("div", {
|
|
985
|
+
"data-tone": tone,
|
|
986
|
+
...tone === "fail" ? { role: "alert" } : {}
|
|
987
|
+
}, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
|
|
988
|
+
}
|
|
989
|
+
/** One classified failure as a reader-facing sentence. */
|
|
990
|
+
function failureText(failure, t) {
|
|
991
|
+
switch (failure.kind) {
|
|
992
|
+
case "unauthenticated": return t("failureUnauthorized");
|
|
993
|
+
case "forbidden": return t("failureForbidden");
|
|
994
|
+
case "unmounted": return t("failureUnmounted");
|
|
995
|
+
case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
|
|
996
|
+
case "refused": return failure.message;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
|
|
1000
|
+
function FailureBlock(props) {
|
|
1001
|
+
const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
|
|
1002
|
+
return Block({
|
|
1003
|
+
tone: "fail",
|
|
1004
|
+
title: failureText(props.failure, props.t),
|
|
1005
|
+
children: hint
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/client/fallbacks.generated.ts
|
|
1010
|
+
/**
|
|
1011
|
+
* The command-face fallback for every Host surface, generated from evolution-commands'
|
|
1012
|
+
* COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
|
|
1013
|
+
* panels render this hint instead of a bare HTTP failure — same use case, command face.
|
|
1014
|
+
*/
|
|
1015
|
+
const FALLBACKS = {
|
|
1016
|
+
approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
|
|
1017
|
+
"skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
|
|
1018
|
+
"skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
|
|
1019
|
+
diagnostics: {
|
|
1020
|
+
command: "/evolution doctor",
|
|
1021
|
+
platformCli: "dsh --json /evolution doctor"
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
//#endregion
|
|
851
1025
|
//#region src/client/Panel.ts
|
|
852
1026
|
/**
|
|
853
1027
|
* The panel: one read, one open skill, one open row — and the faces that draw them.
|
|
@@ -865,9 +1039,29 @@ window.__ModuleLoader__.load({
|
|
|
865
1039
|
* @param face - copy plus the data callbacks.
|
|
866
1040
|
* @returns the panel element.
|
|
867
1041
|
*/
|
|
1042
|
+
/** Fill the generated block's `{slot}` copies with this bundle's dictionary. */
|
|
1043
|
+
function fillCopy(template, values) {
|
|
1044
|
+
return template.replace(/\{(\w+)\}/g, (match, slot) => {
|
|
1045
|
+
const value = values[slot];
|
|
1046
|
+
return value === void 0 ? match : String(value);
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
/** The shared four-state failure block, spoken through this bundle's keys. */
|
|
1050
|
+
function failureBlock(t, failure) {
|
|
1051
|
+
const translate = (key, values) => {
|
|
1052
|
+
const text = t(key);
|
|
1053
|
+
return values === void 0 ? text : fillCopy(text, values);
|
|
1054
|
+
};
|
|
1055
|
+
return FailureBlock({
|
|
1056
|
+
failure,
|
|
1057
|
+
t: translate,
|
|
1058
|
+
fallback: FALLBACKS["skill-history"]
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
868
1061
|
function SkillHistoryPanel(face) {
|
|
869
1062
|
const [skills, setSkills] = (0, react.useState)([]);
|
|
870
1063
|
const [skillsLoaded, setSkillsLoaded] = (0, react.useState)(false);
|
|
1064
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
871
1065
|
const [selected, setSelected] = (0, react.useState)(void 0);
|
|
872
1066
|
const [payload, setPayload] = (0, react.useState)(void 0);
|
|
873
1067
|
const [versionsFailed, setVersionsFailed] = (0, react.useState)(void 0);
|
|
@@ -882,6 +1076,10 @@ window.__ModuleLoader__.load({
|
|
|
882
1076
|
const failed = (error) => {
|
|
883
1077
|
setNoteError(true);
|
|
884
1078
|
setNoteText(face.t("error.hint") + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
|
|
1079
|
+
setFailure(error instanceof SkillHistoryRefusal ? error.failure : {
|
|
1080
|
+
kind: "server",
|
|
1081
|
+
status: 0
|
|
1082
|
+
});
|
|
885
1083
|
};
|
|
886
1084
|
const succeeded = (message) => {
|
|
887
1085
|
setNoteError(false);
|
|
@@ -1055,7 +1253,7 @@ window.__ModuleLoader__.load({
|
|
|
1055
1253
|
};
|
|
1056
1254
|
return (0, react.createElement)("div", { className: "evo-hist-root" }, (0, react.createElement)("aside", { className: "evo-hist-aside" }, searchField(face, query, setQuery), (0, react.createElement)("div", { className: "evo-hist-list" }, skills.length === 0 ? emptyState(face.t(skillsLoaded ? "empty.skills" : "loading")) : visible.length === 0 ? emptyState(face.t("search.none")) : visible.map((skill) => skillLine(face, skill, skill.name === selected, () => {
|
|
1057
1255
|
open(skill.name);
|
|
1058
|
-
})))), (0, react.createElement)("div", { className: "evo-hist-main" }, panelHead(face, refresh), notice(noteText, noteError), selected === void 0 ? [emptyState(face.t(skills.length === 0 ? skillsLoaded ? "empty.skills" : "loading" : "empty.pick"), "empty"), (0, react.createElement)("p", {
|
|
1256
|
+
})))), (0, react.createElement)("div", { className: "evo-hist-main" }, panelHead(face, refresh), notice(noteText, noteError), failure === void 0 ? null : (0, react.createElement)("div", { className: "evo-hist-failure" }, failureBlock(face.t, failure)), selected === void 0 ? [emptyState(face.t(skills.length === 0 ? skillsLoaded ? "empty.skills" : "loading" : "empty.pick"), "empty"), (0, react.createElement)("p", {
|
|
1059
1257
|
key: "hint",
|
|
1060
1258
|
className: "evo-hist-hint"
|
|
1061
1259
|
}, face.t("hint"))] : loaded ? null : emptyState(versionsFailed === void 0 ? face.t("loading") : `${face.t("empty.versions.failed")} — ${versionsFailed}`), loaded ? (0, react.createElement)("div", null, group(face.t("group.content"), payload.content, void 0), group(face.t("group.support"), payload.support, face.t("group.support.note")), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("empty.versions")) : null) : null));
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HOST_ROUTES, json, methodNotAllowed, readJsonObject, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
|
|
2
|
+
import { contentHash, displayBodyOf, elapsedSince, errorText, isLandmark, latestVersionAt, partitionVersions, skillRowCells, textDiffFacts, versionActionKind, versionRowCells } from "@lmzhen/dsh-evolution-core";
|
|
2
3
|
//#region lib/types/unit-undoable.js
|
|
3
4
|
/**
|
|
4
5
|
* G3b: undoability is a property of the UNIT, not of one entry.
|
|
@@ -36,22 +37,20 @@ function undoableFor(entry, flags, liveHash) {
|
|
|
36
37
|
//#endregion
|
|
37
38
|
//#region lib/types/routes.js
|
|
38
39
|
/**
|
|
39
|
-
* The skill-history HTTP surface: six
|
|
40
|
+
* The skill-history HTTP surface: six exact Fetch routes over the curator's read/write seam (five
|
|
41
|
+
* reads plus the one write).
|
|
42
|
+
*
|
|
43
|
+
* W6/G4: the carrier is the platform's Host connection data plane — the fence, browser auth and
|
|
44
|
+
* owner-only admission are applied by the platform BEFORE `fetch(request)` runs, so this module has
|
|
45
|
+
* no 403/401 branch. The wire envelope the browser half reads is unchanged.
|
|
40
46
|
*
|
|
41
47
|
* Layering: this module maps a route to a USE CASE and does nothing else. "Which artifact does this
|
|
42
48
|
* version hold" and "how do the two chains split" live in evolution-core; the only write is
|
|
43
49
|
* curator.undo. The panel never writes the skill tree itself.
|
|
44
50
|
* @module @lmzhen/dsh-evolution-skill-history/routes
|
|
45
51
|
*/
|
|
46
|
-
/** Route paths
|
|
47
|
-
const SKILL_HISTORY_ROUTES =
|
|
48
|
-
skills: "/api/dsh-evolution/skill-history/skills",
|
|
49
|
-
versions: "/api/dsh-evolution/skill-history/versions",
|
|
50
|
-
diff: "/api/dsh-evolution/skill-history/versions/diff",
|
|
51
|
-
body: "/api/dsh-evolution/skill-history/versions/body",
|
|
52
|
-
undo: "/api/dsh-evolution/skill-history/undo",
|
|
53
|
-
health: "/api/dsh-evolution/skill-history/health"
|
|
54
|
-
};
|
|
52
|
+
/** Route paths: the ONE declaration lives in core (W1); the client constants are generated from it. */
|
|
53
|
+
const SKILL_HISTORY_ROUTES = HOST_ROUTES.skillHistory;
|
|
55
54
|
/**
|
|
56
55
|
* How much of one version's body the read route hands back. A bound on the ANSWER, not on what the
|
|
57
56
|
* deployment may store: a long skill arrives as its head plus `truncated`, so the reader is told.
|
|
@@ -60,49 +59,29 @@ const MAX_BODY_CHARS = 2e4;
|
|
|
60
59
|
/**
|
|
61
60
|
* Build the six routes over the curator seam.
|
|
62
61
|
* @param services - the host services the handlers read.
|
|
63
|
-
* @
|
|
62
|
+
* @param _audit - the registration audit (read routes record nothing; undo's decision stays the
|
|
63
|
+
* curator's own — W6 keeps the ledger to the approval surface).
|
|
64
|
+
* @returns the route list for `registerHostRoutes`.
|
|
64
65
|
*/
|
|
65
|
-
function makeSkillHistoryRoutes(services) {
|
|
66
|
+
function makeSkillHistoryRoutes(services, _audit) {
|
|
66
67
|
/** Run one handler body with the curator, or answer the family's E-302 sentence when it is absent. */
|
|
67
|
-
const withCurator = async (
|
|
68
|
+
const withCurator = async (run) => {
|
|
68
69
|
const curator = services.getCurator();
|
|
69
|
-
if (curator === void 0) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
await run(curator);
|
|
78
|
-
};
|
|
79
|
-
/** Guard: trust fence, then method. Every refusal is a JSON body, never an empty response. */
|
|
80
|
-
const guard = (req, res, method) => {
|
|
81
|
-
if (!isLoopbackRequest(req)) {
|
|
82
|
-
writeJson(res, 403, {
|
|
83
|
-
ok: false,
|
|
84
|
-
code: "forbidden",
|
|
85
|
-
message: "禁止访问:仅限回环地址"
|
|
86
|
-
});
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
if (req.method !== method) {
|
|
90
|
-
writeJson(res, 405, {
|
|
91
|
-
ok: false,
|
|
92
|
-
code: "method-not-allowed",
|
|
93
|
-
message: "方法不允许:" + (req.method ?? "")
|
|
94
|
-
});
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
return true;
|
|
70
|
+
if (curator === void 0) return json(200, {
|
|
71
|
+
ok: false,
|
|
72
|
+
code: "curator-not-mounted",
|
|
73
|
+
message: errorText("e-302-curator-service-not-mounted")
|
|
74
|
+
});
|
|
75
|
+
return await run(curator);
|
|
98
76
|
};
|
|
99
77
|
return [
|
|
100
78
|
{
|
|
101
|
-
kind: "exact",
|
|
102
79
|
path: SKILL_HISTORY_ROUTES.skills,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
methods: ["GET"],
|
|
81
|
+
requestBody: "buffered",
|
|
82
|
+
fetch: (request) => {
|
|
83
|
+
if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
|
|
84
|
+
return withCurator(async (curator) => {
|
|
106
85
|
const listed = await curator.skills.list();
|
|
107
86
|
const withHistory = [];
|
|
108
87
|
const now = Date.now();
|
|
@@ -125,7 +104,7 @@ function makeSkillHistoryRoutes(services) {
|
|
|
125
104
|
cells: skillRowCells(row)
|
|
126
105
|
});
|
|
127
106
|
}
|
|
128
|
-
|
|
107
|
+
return json(200, {
|
|
129
108
|
ok: true,
|
|
130
109
|
data: withHistory
|
|
131
110
|
});
|
|
@@ -133,20 +112,18 @@ function makeSkillHistoryRoutes(services) {
|
|
|
133
112
|
}
|
|
134
113
|
},
|
|
135
114
|
{
|
|
136
|
-
kind: "exact",
|
|
137
115
|
path: SKILL_HISTORY_ROUTES.versions,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await withCurator(res, async (curator) => {
|
|
116
|
+
methods: ["GET"],
|
|
117
|
+
requestBody: "buffered",
|
|
118
|
+
fetch: (request) => {
|
|
119
|
+
if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
|
|
120
|
+
const name = new URL(request.url).searchParams.get("name")?.trim() ?? "";
|
|
121
|
+
if (name === "") return Promise.resolve(json(400, {
|
|
122
|
+
ok: false,
|
|
123
|
+
code: "bad-request",
|
|
124
|
+
message: "缺少 name"
|
|
125
|
+
}));
|
|
126
|
+
return withCurator(async (curator) => {
|
|
150
127
|
const groups = partitionVersions(await curator.history(name));
|
|
151
128
|
const live = await curator.skills.read(name);
|
|
152
129
|
const liveHash = live === null ? null : contentHash(live);
|
|
@@ -161,7 +138,7 @@ function makeSkillHistoryRoutes(services) {
|
|
|
161
138
|
};
|
|
162
139
|
};
|
|
163
140
|
const unitFlags = unitUndoableFlags([...groups.content, ...groups.support], liveHash);
|
|
164
|
-
|
|
141
|
+
return json(200, {
|
|
165
142
|
ok: true,
|
|
166
143
|
data: {
|
|
167
144
|
content: groups.content.map((entry, index) => {
|
|
@@ -189,47 +166,39 @@ function makeSkillHistoryRoutes(services) {
|
|
|
189
166
|
}
|
|
190
167
|
},
|
|
191
168
|
{
|
|
192
|
-
kind: "exact",
|
|
193
169
|
path: SKILL_HISTORY_ROUTES.diff,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
170
|
+
methods: ["GET"],
|
|
171
|
+
requestBody: "buffered",
|
|
172
|
+
fetch: (request) => {
|
|
173
|
+
if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
|
|
174
|
+
const params = new URL(request.url).searchParams;
|
|
197
175
|
const name = params.get("name")?.trim() ?? "";
|
|
198
176
|
const rawV = params.get("v")?.trim() ?? "";
|
|
199
177
|
const v = Number.parseInt(rawV, 10);
|
|
200
|
-
if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
await withCurator(res, async (curator) => {
|
|
178
|
+
if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) return Promise.resolve(json(400, {
|
|
179
|
+
ok: false,
|
|
180
|
+
code: "bad-request",
|
|
181
|
+
message: "需要 name 与正整数 v"
|
|
182
|
+
}));
|
|
183
|
+
return withCurator(async (curator) => {
|
|
209
184
|
const groups = partitionVersions(await curator.history(name));
|
|
210
185
|
const index = groups.content.findIndex((entry) => entry.v === v);
|
|
211
|
-
if (index === -1) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
186
|
+
if (index === -1) return json(404, {
|
|
187
|
+
ok: false,
|
|
188
|
+
code: "not-found",
|
|
189
|
+
message: "该版本不在「" + name + "」的记录历史里"
|
|
190
|
+
});
|
|
219
191
|
const entry = groups.content[index];
|
|
220
192
|
const previous = (entry === void 0 || entry.beforeHash === void 0 ? void 0 : groups.content.find((candidate) => candidate.hash === entry.beforeHash)) ?? (index === 0 ? void 0 : groups.content[index - 1]);
|
|
221
193
|
const after = entry === void 0 ? null : await curator.skills.readVersion(name, v);
|
|
222
194
|
const before = previous === void 0 ? "" : await curator.skills.readVersion(name, previous.v);
|
|
223
|
-
if (after === null || before === null) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
});
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
195
|
+
if (after === null || before === null) return json(404, {
|
|
196
|
+
ok: false,
|
|
197
|
+
code: "not-found",
|
|
198
|
+
message: "该版本的存储内容读不出来"
|
|
199
|
+
});
|
|
231
200
|
const facts = textDiffFacts(before, after, "SKILL.md");
|
|
232
|
-
|
|
201
|
+
return json(200, {
|
|
233
202
|
ok: true,
|
|
234
203
|
data: {
|
|
235
204
|
v,
|
|
@@ -241,34 +210,29 @@ function makeSkillHistoryRoutes(services) {
|
|
|
241
210
|
}
|
|
242
211
|
},
|
|
243
212
|
{
|
|
244
|
-
kind: "exact",
|
|
245
213
|
path: SKILL_HISTORY_ROUTES.body,
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
214
|
+
methods: ["GET"],
|
|
215
|
+
requestBody: "buffered",
|
|
216
|
+
fetch: (request) => {
|
|
217
|
+
if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
|
|
218
|
+
const params = new URL(request.url).searchParams;
|
|
249
219
|
const name = params.get("name")?.trim() ?? "";
|
|
250
220
|
const rawV = params.get("v")?.trim() ?? "";
|
|
251
221
|
const v = Number.parseInt(rawV, 10);
|
|
252
|
-
if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) {
|
|
253
|
-
|
|
222
|
+
if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) return Promise.resolve(json(400, {
|
|
223
|
+
ok: false,
|
|
224
|
+
code: "bad-request",
|
|
225
|
+
message: "需要 name 与正整数 v"
|
|
226
|
+
}));
|
|
227
|
+
return withCurator(async (curator) => {
|
|
228
|
+
const text = await curator.skills.readVersion(name, v);
|
|
229
|
+
if (text === null) return json(404, {
|
|
254
230
|
ok: false,
|
|
255
|
-
code: "
|
|
256
|
-
message: "
|
|
231
|
+
code: "not-found",
|
|
232
|
+
message: "该版本的存储内容读不出来"
|
|
257
233
|
});
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
await withCurator(res, async (curator) => {
|
|
261
|
-
const text = await curator.skills.readVersion(name, v);
|
|
262
|
-
if (text === null) {
|
|
263
|
-
writeJson(res, 404, {
|
|
264
|
-
ok: false,
|
|
265
|
-
code: "not-found",
|
|
266
|
-
message: "该版本的存储内容读不出来"
|
|
267
|
-
});
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
234
|
const truncated = text.length > MAX_BODY_CHARS;
|
|
271
|
-
|
|
235
|
+
return json(200, {
|
|
272
236
|
ok: true,
|
|
273
237
|
data: {
|
|
274
238
|
v,
|
|
@@ -281,89 +245,66 @@ function makeSkillHistoryRoutes(services) {
|
|
|
281
245
|
}
|
|
282
246
|
},
|
|
283
247
|
{
|
|
284
|
-
kind: "exact",
|
|
285
248
|
path: SKILL_HISTORY_ROUTES.undo,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
249
|
+
methods: ["POST"],
|
|
250
|
+
requestBody: "buffered",
|
|
251
|
+
fetch: (request) => {
|
|
252
|
+
if (request.method !== "POST") return Promise.resolve(methodNotAllowed(["POST"]));
|
|
253
|
+
return (async () => {
|
|
254
|
+
const payload = await readJsonObject(request);
|
|
255
|
+
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
|
256
|
+
if (name === "") return json(400, {
|
|
292
257
|
ok: false,
|
|
293
258
|
code: "bad-request",
|
|
294
259
|
message: "缺少 name"
|
|
295
260
|
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const raw = payload?.v;
|
|
299
|
-
if (raw !== void 0 && (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1)) {
|
|
300
|
-
writeJson(res, 400, {
|
|
261
|
+
const raw = payload?.v;
|
|
262
|
+
if (raw !== void 0 && (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1)) return json(400, {
|
|
301
263
|
ok: false,
|
|
302
264
|
code: "bad-request",
|
|
303
265
|
message: "v 必须是正整数"
|
|
304
266
|
});
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
message: result.message
|
|
267
|
+
return withCurator(async (curator) => {
|
|
268
|
+
const result = raw === void 0 ? await curator.undo(name) : await curator.undo(name, raw);
|
|
269
|
+
return json(200, result.ok ? {
|
|
270
|
+
ok: true,
|
|
271
|
+
data: result
|
|
272
|
+
} : {
|
|
273
|
+
ok: false,
|
|
274
|
+
code: "undo-refused",
|
|
275
|
+
message: result.message
|
|
276
|
+
});
|
|
316
277
|
});
|
|
317
|
-
});
|
|
278
|
+
})();
|
|
318
279
|
}
|
|
319
280
|
},
|
|
320
281
|
{
|
|
321
|
-
kind: "exact",
|
|
322
282
|
path: SKILL_HISTORY_ROUTES.health,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
283
|
+
methods: ["GET"],
|
|
284
|
+
requestBody: "buffered",
|
|
285
|
+
fetch: (request) => {
|
|
286
|
+
if (request.method !== "GET") return Promise.resolve(methodNotAllowed(["GET"]));
|
|
287
|
+
return Promise.resolve(json(200, {
|
|
326
288
|
ok: true,
|
|
327
289
|
data: { surface: "skill-history" }
|
|
328
|
-
});
|
|
290
|
+
}));
|
|
329
291
|
}
|
|
330
292
|
}
|
|
331
293
|
];
|
|
332
294
|
}
|
|
333
295
|
//#endregion
|
|
334
296
|
//#region lib/types/index.js
|
|
335
|
-
/**
|
|
336
|
-
* Host half of the skill-history surface: it mounts the four loopback routes the sidebar panel reads
|
|
337
|
-
* and writes, and it owns nothing else.
|
|
338
|
-
*
|
|
339
|
-
* The version FACTS come from evolution-core (which artifact a version holds, how the body chain and
|
|
340
|
-
* the support-file chain split) and the only write is the curator's undo, so this row carries no state
|
|
341
|
-
* of its own: unloading it removes the routes and leaves the feature's data untouched.
|
|
342
|
-
* @module @lmzhen/dsh-evolution-skill-history
|
|
343
|
-
*/
|
|
344
297
|
const name = "evolution-skill-history";
|
|
345
298
|
/** No declared service: a headless profile must not leave a pending row, and the curator is resolved
|
|
346
|
-
* per request
|
|
299
|
+
* per request; `connection` is waited on inside registerHostRoutes, not demanded here. */
|
|
347
300
|
const inject = [];
|
|
348
301
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* the web server may not exist yet — `ctx.get('webServer')` returning undefined is a TIMING fact, not a
|
|
353
|
-
* verdict, and acting on it (0.11.1 shipped that way) left the routes permanently unregistered while the
|
|
354
|
-
* panel rendered and every call answered HTTP 404. Waiting on the service mounts the routes whenever it
|
|
355
|
-
* arrives, and a profile that never gets one simply never runs this callback — inert, never pending.
|
|
356
|
-
* @param ctx - the host context; the web server and curator arrive through it.
|
|
302
|
+
* Mount the routes as ONE owner effect (disposers run on unload and on HMR). A profile whose host has
|
|
303
|
+
* no connection service simply never runs the callback — inert, never pending.
|
|
304
|
+
* @param ctx - the host context; the connection and curator arrive through it.
|
|
357
305
|
*/
|
|
358
306
|
function apply(ctx) {
|
|
359
|
-
ctx
|
|
360
|
-
scope.effect(() => {
|
|
361
|
-
const disposers = makeSkillHistoryRoutes({ getCurator: () => scope.get("evolutionCurator") }).map((route) => scope.webServer.register(route));
|
|
362
|
-
return () => {
|
|
363
|
-
for (const dispose of disposers) dispose();
|
|
364
|
-
};
|
|
365
|
-
}, "evolution-skill-history: routes");
|
|
366
|
-
});
|
|
307
|
+
registerHostRoutes(ctx, (audit) => makeSkillHistoryRoutes({ getCurator: () => ctx.get("evolutionCurator") }, audit));
|
|
367
308
|
}
|
|
368
309
|
//#endregion
|
|
369
310
|
export { apply, inject, name };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type ReactNode } from 'react';
|
|
13
13
|
import { type Copy } from './chrome.ts';
|
|
14
|
-
import type
|
|
14
|
+
import { type SkillRow, type VersionBodyRow, type VersionDiffRow, type VersionsPayload } from './api.ts';
|
|
15
15
|
import type { MarkdownWords } from './markdown.ts';
|
|
16
16
|
/** What the panel is handed: the dictionary seat, the renderer's words, and the host's callbacks. */
|
|
17
17
|
export interface PanelFace extends Copy {
|
|
@@ -23,10 +23,5 @@ export interface PanelFace extends Copy {
|
|
|
23
23
|
readonly loadBody: (name: string, v: number) => Promise<VersionBodyRow>;
|
|
24
24
|
readonly undo: (name: string, v?: number) => Promise<string>;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
* The panel body.
|
|
28
|
-
* @param face - copy plus the data callbacks.
|
|
29
|
-
* @returns the panel element.
|
|
30
|
-
*/
|
|
31
26
|
export declare function SkillHistoryPanel(face: PanelFace): ReactNode;
|
|
32
27
|
//# sourceMappingURL=Panel.d.ts.map
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* The paths mirror the host half's `SKILL_HISTORY_ROUTES` (a spec asserts the two sides agree). Every
|
|
5
|
-
* refusal arrives as `ok:false` plus the curator's own sentence, so the panel shows the same words the
|
|
6
|
-
* slash command prints instead of inventing a second vocabulary.
|
|
7
|
-
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
8
|
-
*/
|
|
9
|
-
/** Route paths, mirrored from the host half. */
|
|
1
|
+
import type { RouteFailure } from './route-client.generated.ts';
|
|
2
|
+
/** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
|
|
10
3
|
export declare const HOST_ROUTES: {
|
|
11
4
|
readonly skills: "/api/dsh-evolution/skill-history/skills";
|
|
12
5
|
readonly versions: "/api/dsh-evolution/skill-history/versions";
|
|
13
6
|
readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
|
|
14
7
|
readonly body: "/api/dsh-evolution/skill-history/versions/body";
|
|
15
8
|
readonly undo: "/api/dsh-evolution/skill-history/undo";
|
|
9
|
+
readonly health: "/api/dsh-evolution/skill-history/health";
|
|
16
10
|
};
|
|
17
11
|
/** One changed region, as the host reports it (the platform diff card's own shape). */
|
|
18
12
|
export interface DiffHunkRow {
|
|
@@ -129,8 +123,13 @@ export interface SkillHistoryApi {
|
|
|
129
123
|
readonly diff: (name: string, v: number) => Promise<VersionDiffRow>;
|
|
130
124
|
readonly body: (name: string, v: number) => Promise<VersionBodyRow>;
|
|
131
125
|
}
|
|
132
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* A refusal the host reported, carrying its own sentence (the curator's, not ours) and the classified
|
|
128
|
+
* four-state failure so the panel can render the shared failure block (G5/W7 S6b).
|
|
129
|
+
*/
|
|
133
130
|
export declare class SkillHistoryRefusal extends Error {
|
|
131
|
+
readonly failure: RouteFailure;
|
|
132
|
+
constructor(failure: RouteFailure);
|
|
134
133
|
}
|
|
135
134
|
/**
|
|
136
135
|
* Build the panel's face over one fetch implementation.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Literal mirror of the frozen Host diagnostics contract (W1 §2.8 / core diagnostics.ts). The browser
|
|
3
|
+
* build keeps every @deepseek-ai/* specifier external, so the client CANNOT import the host type — the
|
|
4
|
+
* shape is mirrored mechanically here instead. The key unions are rendered from core diagnostics.ts by
|
|
5
|
+
* the generator, so a key change regenerates all three copies (a stale copy fails --check).
|
|
6
|
+
*/
|
|
7
|
+
export type DiagnosticsSchemaVersion = 2;
|
|
8
|
+
export type DiagnosticsFactKey = 'platform-line' | 'family-version' | 'install-form' | 'preset-ids' | 'readonly-contract' | 'upgrade-order' | 'fallback-map';
|
|
9
|
+
export type DiagnosticsRuntimeKey = 'subagent-channel' | 'restricted-preset' | 'host-surface' | 'session-fold' | 'platform-services' | 'runs' | 'platform-pairing' | 'install-form-detected';
|
|
10
|
+
export type DiagnosticStatus = 'ok' | 'warn' | 'fail' | 'unknown';
|
|
11
|
+
export interface DocFactRow {
|
|
12
|
+
readonly key: DiagnosticsFactKey;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly value: string;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DiagnosticRow {
|
|
18
|
+
readonly key: DiagnosticsRuntimeKey;
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly status: DiagnosticStatus;
|
|
21
|
+
readonly detail?: string;
|
|
22
|
+
readonly remedy?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface DiagnosticsView {
|
|
25
|
+
readonly schemaVersion: DiagnosticsSchemaVersion;
|
|
26
|
+
readonly generatedAt: string;
|
|
27
|
+
readonly facts: readonly DocFactRow[];
|
|
28
|
+
readonly runtime: readonly DiagnosticRow[];
|
|
29
|
+
}
|
|
30
|
+
/** The installation form the doctor reports (W8 install-form-detected row). */
|
|
31
|
+
export type InstallFormId = 'M1' | 'M2' | 'M3' | 'M4';
|
|
32
|
+
/** One surface's command-face path(s) — the client `fallbacks.generated.ts` table's row type. */
|
|
33
|
+
export interface FallbackHint {
|
|
34
|
+
readonly command: string;
|
|
35
|
+
readonly platformCli?: string;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=contracts.generated.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FallbackHint } from './contracts.generated.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The command-face fallback for every Host surface, generated from evolution-commands'
|
|
4
|
+
* COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
|
|
5
|
+
* panels render this hint instead of a bare HTTP failure — same use case, command face.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FALLBACKS: Readonly<Record<'approval' | 'skill-history' | 'skill-health' | 'diagnostics', FallbackHint>>;
|
|
8
|
+
//# sourceMappingURL=fallbacks.generated.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four-state transport classifier every family client shares (W7 §2). The platform owns
|
|
3
|
+
* admission (401/403/404 are its answers, not ours), so the client reads the STATUS only for those
|
|
4
|
+
* three and never tries to interpret a platform body. Everything the family route itself answers
|
|
5
|
+
* keeps its business envelope: 2xx + {ok:true,data} or 2xx + {ok:false,code,message}.
|
|
6
|
+
*/
|
|
7
|
+
export type RouteFailure = {
|
|
8
|
+
readonly kind: 'unauthenticated';
|
|
9
|
+
readonly status: 401;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'forbidden';
|
|
12
|
+
readonly status: 403;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'unmounted';
|
|
15
|
+
readonly status: 404;
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: 'server';
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly detail?: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'refused';
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly message: string;
|
|
24
|
+
};
|
|
25
|
+
/** One route answer: the data, or the classified failure. */
|
|
26
|
+
export type RouteAnswer<T> = {
|
|
27
|
+
readonly ok: true;
|
|
28
|
+
readonly data: T;
|
|
29
|
+
} | {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly failure: RouteFailure;
|
|
32
|
+
};
|
|
33
|
+
/** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
|
|
34
|
+
export declare function classify(status: number): RouteFailure;
|
|
35
|
+
/**
|
|
36
|
+
* One request, no retries and no body parsing for the platform's admission answers.
|
|
37
|
+
* @param doFetch - the fetch in use (the browser's, or a spec's stub).
|
|
38
|
+
* @param path - the exact route path from the generated ROUTES table.
|
|
39
|
+
* @param init - standard RequestInit.
|
|
40
|
+
* @returns the data, or the classified failure (network/parse failures read as 'server').
|
|
41
|
+
*/
|
|
42
|
+
export declare function requestJson<T>(doFetch: typeof fetch, path: string, init?: RequestInit): Promise<RouteAnswer<T>>;
|
|
43
|
+
/** The one-sentence rendering legacy callers attach to a classified failure. */
|
|
44
|
+
export declare function failureMessage(failure: RouteFailure): string;
|
|
45
|
+
//# sourceMappingURL=route-client.generated.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** One host data-plane surface the browser half can call. */
|
|
2
|
+
export type SurfaceId = 'approval' | 'skill-history' | 'skill-health' | 'diagnostics';
|
|
3
|
+
/**
|
|
4
|
+
* Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
|
|
5
|
+
* A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
|
|
6
|
+
* external), so the table is mirrored here mechanically; a rename in core regenerates all three
|
|
7
|
+
* client copies, and the generator's --check makes a stale copy red.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ROUTES: {
|
|
10
|
+
readonly approval: {
|
|
11
|
+
readonly pending: "/api/dsh-evolution/approval/pending";
|
|
12
|
+
readonly preview: "/api/dsh-evolution/approval/preview";
|
|
13
|
+
readonly approve: "/api/dsh-evolution/approval/approve";
|
|
14
|
+
readonly reject: "/api/dsh-evolution/approval/reject";
|
|
15
|
+
};
|
|
16
|
+
readonly 'skill-history': {
|
|
17
|
+
readonly skills: "/api/dsh-evolution/skill-history/skills";
|
|
18
|
+
readonly versions: "/api/dsh-evolution/skill-history/versions";
|
|
19
|
+
readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
|
|
20
|
+
readonly body: "/api/dsh-evolution/skill-history/versions/body";
|
|
21
|
+
readonly undo: "/api/dsh-evolution/skill-history/undo";
|
|
22
|
+
readonly health: "/api/dsh-evolution/skill-history/health";
|
|
23
|
+
};
|
|
24
|
+
readonly 'skill-health': {
|
|
25
|
+
readonly rows: "/api/dsh-evolution/skill-health/rows";
|
|
26
|
+
};
|
|
27
|
+
readonly diagnostics: {
|
|
28
|
+
readonly view: "/api/dsh-evolution/diagnostics";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=routes.generated.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { RouteAnswer, RouteFailure } from './route-client.generated.ts';
|
|
3
|
+
import type { FallbackHint } from './contracts.generated.ts';
|
|
4
|
+
/**
|
|
5
|
+
* The four-state base of every family card (W7 §2). Load states are read from the generated
|
|
6
|
+
* route-client's answer — a card owns no HTTP vocabulary and no second state machine.
|
|
7
|
+
*/
|
|
8
|
+
export type LoadState<T> = {
|
|
9
|
+
readonly phase: 'loading';
|
|
10
|
+
} | {
|
|
11
|
+
readonly phase: 'ready';
|
|
12
|
+
readonly data: T;
|
|
13
|
+
} | {
|
|
14
|
+
readonly phase: 'empty';
|
|
15
|
+
} | {
|
|
16
|
+
readonly phase: 'failed';
|
|
17
|
+
readonly failure: RouteFailure;
|
|
18
|
+
};
|
|
19
|
+
/** Map one classified answer onto the card's four states. */
|
|
20
|
+
export declare function loadStateOf<T>(answer: RouteAnswer<T>, isEmpty: (data: T) => boolean): LoadState<T>;
|
|
21
|
+
/** The locale lookup every block calls (the platform's own `t`). */
|
|
22
|
+
export interface Translate {
|
|
23
|
+
(key: string, values?: Readonly<Record<string, string | number>>): string;
|
|
24
|
+
}
|
|
25
|
+
export type BlockTone = 'neutral' | 'warn' | 'fail';
|
|
26
|
+
export interface BlockProps {
|
|
27
|
+
readonly tone?: BlockTone;
|
|
28
|
+
readonly title?: string;
|
|
29
|
+
readonly children?: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
|
|
32
|
+
export declare function Block(props: BlockProps): ReactNode;
|
|
33
|
+
export interface FailureBlockProps {
|
|
34
|
+
readonly failure: RouteFailure;
|
|
35
|
+
readonly t: Translate;
|
|
36
|
+
/** The command-face fallback for this surface (rendered on unmounted/failed answers). */
|
|
37
|
+
readonly fallback?: FallbackHint | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** One classified failure as a reader-facing sentence. */
|
|
40
|
+
export declare function failureText(failure: RouteFailure, t: Translate): string;
|
|
41
|
+
/** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
|
|
42
|
+
export declare function FailureBlock(props: FailureBlockProps): ReactNode;
|
|
43
|
+
//# sourceMappingURL=states.generated.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host half of the skill-history surface: it mounts the
|
|
3
|
-
*
|
|
2
|
+
* Host half of the skill-history surface: it mounts the six routes the sidebar panel reads and
|
|
3
|
+
* writes, and it owns nothing else.
|
|
4
|
+
*
|
|
5
|
+
* W6/G4: the carrier is the platform's Host connection data plane. `registerHostRoutes` waits on the
|
|
6
|
+
* `connection` service (it may apply after this row), registers via one owner effect and captures the
|
|
7
|
+
* DecisionAudit there; the old webServer + loopback fence path is gone.
|
|
4
8
|
*
|
|
5
9
|
* The version FACTS come from evolution-core (which artifact a version holds, how the body chain and
|
|
6
10
|
* the support-file chain split) and the only write is the curator's undo, so this row carries no state
|
|
@@ -10,17 +14,12 @@
|
|
|
10
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
15
|
export declare const name = "evolution-skill-history";
|
|
12
16
|
/** No declared service: a headless profile must not leave a pending row, and the curator is resolved
|
|
13
|
-
* per request
|
|
17
|
+
* per request; `connection` is waited on inside registerHostRoutes, not demanded here. */
|
|
14
18
|
export declare const inject: string[];
|
|
15
19
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the web server may not exist yet — `ctx.get('webServer')` returning undefined is a TIMING fact, not a
|
|
20
|
-
* verdict, and acting on it (0.11.1 shipped that way) left the routes permanently unregistered while the
|
|
21
|
-
* panel rendered and every call answered HTTP 404. Waiting on the service mounts the routes whenever it
|
|
22
|
-
* arrives, and a profile that never gets one simply never runs this callback — inert, never pending.
|
|
23
|
-
* @param ctx - the host context; the web server and curator arrive through it.
|
|
20
|
+
* Mount the routes as ONE owner effect (disposers run on unload and on HMR). A profile whose host has
|
|
21
|
+
* no connection service simply never runs the callback — inert, never pending.
|
|
22
|
+
* @param ctx - the host context; the connection and curator arrive through it.
|
|
24
23
|
*/
|
|
25
24
|
export declare function apply(ctx: Context): void;
|
|
26
25
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The skill-history HTTP surface: six
|
|
2
|
+
* The skill-history HTTP surface: six exact Fetch routes over the curator's read/write seam (five
|
|
3
|
+
* reads plus the one write).
|
|
4
|
+
*
|
|
5
|
+
* W6/G4: the carrier is the platform's Host connection data plane — the fence, browser auth and
|
|
6
|
+
* owner-only admission are applied by the platform BEFORE `fetch(request)` runs, so this module has
|
|
7
|
+
* no 403/401 branch. The wire envelope the browser half reads is unchanged.
|
|
3
8
|
*
|
|
4
9
|
* Layering: this module maps a route to a USE CASE and does nothing else. "Which artifact does this
|
|
5
10
|
* version hold" and "how do the two chains split" live in evolution-core; the only write is
|
|
@@ -7,8 +12,8 @@
|
|
|
7
12
|
* @module @lmzhen/dsh-evolution-skill-history/routes
|
|
8
13
|
*/
|
|
9
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
-
import type {
|
|
11
|
-
/** Route paths
|
|
15
|
+
import type { ConnectionFetchRouteLike, DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
|
|
16
|
+
/** Route paths: the ONE declaration lives in core (W1); the client constants are generated from it. */
|
|
12
17
|
export declare const SKILL_HISTORY_ROUTES: {
|
|
13
18
|
readonly skills: "/api/dsh-evolution/skill-history/skills";
|
|
14
19
|
readonly versions: "/api/dsh-evolution/skill-history/versions";
|
|
@@ -37,7 +42,9 @@ export interface RouteServices {
|
|
|
37
42
|
/**
|
|
38
43
|
* Build the six routes over the curator seam.
|
|
39
44
|
* @param services - the host services the handlers read.
|
|
40
|
-
* @
|
|
45
|
+
* @param _audit - the registration audit (read routes record nothing; undo's decision stays the
|
|
46
|
+
* curator's own — W6 keeps the ledger to the approval surface).
|
|
47
|
+
* @returns the route list for `registerHostRoutes`.
|
|
41
48
|
*/
|
|
42
|
-
export declare function makeSkillHistoryRoutes(services: RouteServices):
|
|
49
|
+
export declare function makeSkillHistoryRoutes(services: RouteServices, _audit: DecisionAudit): ConnectionFetchRouteLike[];
|
|
43
50
|
//# sourceMappingURL=routes.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-skill-history",
|
|
3
3
|
"description": "Web surface for skill content history: loopback host routes plus the sidebar panel (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.27.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,23 +30,22 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
33
|
+
"@lmzhen/dsh-evolution-core": "^0.27.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
37
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
36
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
38
37
|
"react": "^18.2.0",
|
|
39
|
-
"@lmzhen/dsh-evolution-
|
|
38
|
+
"@lmzhen/dsh-evolution-core": "^0.27.0",
|
|
39
|
+
"@lmzhen/dsh-evolution-curator": "^0.27.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
43
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
44
43
|
"@types/react": "~18.3.1",
|
|
45
44
|
"react": "^18.2.0",
|
|
46
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
47
|
-
"@lmzhen/dsh-evolution-curator": "^0.
|
|
48
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
49
|
-
"@lmzhen/dsh-evolution-io-node": "^0.
|
|
45
|
+
"@lmzhen/dsh-evolution-core": "^0.27.0",
|
|
46
|
+
"@lmzhen/dsh-evolution-curator": "^0.27.0",
|
|
47
|
+
"@lmzhen/dsh-evolution-io": "^0.27.0",
|
|
48
|
+
"@lmzhen/dsh-evolution-io-node": "^0.27.0"
|
|
50
49
|
},
|
|
51
50
|
"dsh": {
|
|
52
51
|
"client": {
|
|
@@ -57,5 +56,8 @@
|
|
|
57
56
|
"@deepseek-ai/dsh-client-locale"
|
|
58
57
|
]
|
|
59
58
|
}
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"dsh": ">=0.2.1-rc.1 <0.2.2-0"
|
|
60
62
|
}
|
|
61
63
|
}
|