hiwork-core 0.0.11 → 0.0.16

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 CHANGED
@@ -90,6 +90,11 @@ var hostHideCss = [
90
90
  `header [class*="moreButton"] { display: none !important; }`,
91
91
  `[class*="settingsArea"] [aria-haspopup="dialog"] { display: none !important; }`
92
92
  ].join("\n");
93
+ var composerCss = [
94
+ `[data-composer-card]:has([data-composer-input]) { --dsw-elevation-stroke-color: var(--dsw-alias-border-l4); }`,
95
+ `[data-composer-card]:has([data-composer-input]):focus-within { --dsw-elevation-stroke-color: ${brandAccent}; }`,
96
+ `[data-composer-card]:has([data-composer-input]) > * { --dsw-elevation-stroke-color: var(--dsw-alias-border-l4); }`
97
+ ].join("\n");
93
98
  var brandCss = [
94
99
  `body[data-ds-dark-theme] .hiwork-brand-wordmark { filter: invert(1) brightness(1.35); }`
95
100
  ].join("\n");
@@ -724,7 +729,7 @@ function BrandMark(props) {
724
729
  }
725
730
  var brandNameStyle = {
726
731
  display: "inline-flex",
727
- alignItems: "center",
732
+ alignItems: "flex-end",
728
733
  gap: 8,
729
734
  minWidth: 0
730
735
  };
@@ -739,7 +744,8 @@ var versionStyle = {
739
744
  flex: "none",
740
745
  fontSize: 12,
741
746
  fontWeight: 400,
742
- lineHeight: "24px",
747
+ lineHeight: "normal",
748
+ marginBottom: 1,
743
749
  whiteSpace: "nowrap",
744
750
  color: textTertiary
745
751
  };
@@ -3004,6 +3010,464 @@ var ManagedMcpStore = class {
3004
3010
  }
3005
3011
  };
3006
3012
 
3013
+ // src/client/managed-skills.ts
3014
+ var MANAGED_SKILLS_CHANNEL = "/hiwork-managed-skills";
3015
+ var MANAGED_SKILLS_STALE_MS = 5 * 6e4;
3016
+ var ManagedSkillsRpcError = class extends Error {
3017
+ constructor(code, message) {
3018
+ super(message);
3019
+ this.code = code;
3020
+ this.name = "ManagedSkillsRpcError";
3021
+ }
3022
+ };
3023
+ var GENERIC_FAILURE_MESSAGE = "\u6280\u80FD\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
3024
+ var INVALID_RESPONSE_MESSAGE = "\u6280\u80FD\u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002";
3025
+ function unwrapManagedSkillsResult(value) {
3026
+ if (typeof value !== "object" || value === null || !("ok" in value)) {
3027
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE);
3028
+ }
3029
+ const result = value;
3030
+ if (result.ok === true) return result.value;
3031
+ if (result.ok === false) {
3032
+ const code = typeof result.error?.code === "string" ? result.error.code : "internal";
3033
+ const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : GENERIC_FAILURE_MESSAGE;
3034
+ throw new ManagedSkillsRpcError(code, message);
3035
+ }
3036
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE);
3037
+ }
3038
+ function toSnapshot2(value) {
3039
+ if (typeof value !== "object" || value === null) {
3040
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE);
3041
+ }
3042
+ const record = value;
3043
+ const rawList = record["assignments"];
3044
+ if (!Array.isArray(rawList)) {
3045
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE);
3046
+ }
3047
+ const assignments = [];
3048
+ for (const item of rawList) {
3049
+ const normalized = toAssignment2(item);
3050
+ if (normalized !== void 0) assignments.push(normalized);
3051
+ }
3052
+ const lastError = record["lastError"];
3053
+ const snapshot = {
3054
+ revision: typeof record["revision"] === "number" ? record["revision"] : 0,
3055
+ fetchedAt: typeof record["fetchedAt"] === "string" ? record["fetchedAt"] : "",
3056
+ assignments,
3057
+ stale: record["stale"] === true
3058
+ };
3059
+ if (typeof lastError === "object" && lastError !== null) {
3060
+ const detail = lastError;
3061
+ snapshot.lastError = {
3062
+ code: typeof detail.code === "string" ? detail.code : "internal",
3063
+ message: typeof detail.message === "string" ? detail.message : GENERIC_FAILURE_MESSAGE
3064
+ };
3065
+ }
3066
+ return snapshot;
3067
+ }
3068
+ function toAssignment2(value) {
3069
+ if (typeof value !== "object" || value === null) return void 0;
3070
+ const record = value;
3071
+ const assignmentId = text2(record["assignmentId"]);
3072
+ const skillName = text2(record["skillName"]);
3073
+ const contentSha256 = text2(record["contentSha256"]);
3074
+ if (assignmentId === void 0 || skillName === void 0 || contentSha256 === void 0) return void 0;
3075
+ return {
3076
+ assignmentId,
3077
+ revision: typeof record["revision"] === "number" ? record["revision"] : 0,
3078
+ slug: text2(record["slug"]) ?? skillName,
3079
+ displayName: text2(record["displayName"]) ?? skillName,
3080
+ skillName,
3081
+ description: text2(record["description"]) ?? "",
3082
+ contentSha256,
3083
+ sizeBytes: typeof record["sizeBytes"] === "number" ? record["sizeBytes"] : 0,
3084
+ version: text2(record["version"]) ?? "",
3085
+ updatedAt: text2(record["updatedAt"]) ?? ""
3086
+ };
3087
+ }
3088
+ function text2(value) {
3089
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
3090
+ }
3091
+ var ManagedSkillsStore = class {
3092
+ constructor(rpc) {
3093
+ this.rpc = rpc;
3094
+ }
3095
+ current;
3096
+ listeners = /* @__PURE__ */ new Set();
3097
+ /**
3098
+ * 在飞请求**连同它所属的世代**一起记账:只有同一世代的那条才可以被复用。
3099
+ *
3100
+ * 光记 promise 不够——`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,
3101
+ * 它的清单属于上一个人。复用它等于让新账号的同步拿到旧账号的清单,而 `refresh` 的返回值
3102
+ * 是技能域唯一被允许用来删本地受管技能的依据(spec §6 的唯一安全不变量)⇒ 会把新账号
3103
+ * 被分配的技能**硬删掉**。所以要有一个「这条账还算不算数」的判据,见 {@link clear}。
3104
+ */
3105
+ inFlight;
3106
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
3107
+ generation = 0;
3108
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做删除决策**(见文件头)。 */
3109
+ snapshot() {
3110
+ return this.current;
3111
+ }
3112
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
3113
+ isStale(ttlMs = MANAGED_SKILLS_STALE_MS, now = Date.now()) {
3114
+ if (this.current === void 0 || this.current.fetchedAt === "") return true;
3115
+ const fetchedAt = Date.parse(this.current.fetchedAt);
3116
+ if (Number.isNaN(fetchedAt)) return true;
3117
+ return now - fetchedAt >= ttlMs;
3118
+ }
3119
+ /**
3120
+ * 强制同步一次并返回**刚拉到的**清单。
3121
+ *
3122
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次
3123
+ * 网络请求成功了」,用它做删除决策才是安全的;读镜像可能读到上一次的结果。
3124
+ * 失败时抛错且**不动镜像**——调用方据此保持现有的受管技能一条不删(spec §6 的
3125
+ * 唯一安全不变量)。
3126
+ */
3127
+ async refresh(signal) {
3128
+ return await this.dedupe(async (generation) => {
3129
+ const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "refresh", {}, signal);
3130
+ return this.adopt(unwrapManagedSkillsResult(raw), generation);
3131
+ });
3132
+ }
3133
+ /**
3134
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
3135
+ *
3136
+ * 与 {@link refresh} 的分工:这里是「启动时先把界面填上」的便宜入口——命中缓存时
3137
+ * 一次网络请求都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功
3138
+ * 拉取的结果」,但**可能比服务端现状旧**,所以调用方按 `stale` / `isStale()` 判定它
3139
+ * 够不够权威,不够就再 `refresh()` 一次。
3140
+ */
3141
+ async warm(signal) {
3142
+ return await this.dedupe(async (generation) => {
3143
+ const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "snapshot", {}, signal);
3144
+ return this.adopt(unwrapManagedSkillsResult(raw), generation);
3145
+ });
3146
+ }
3147
+ /**
3148
+ * 取一条分配的技能包(base64 字节)。
3149
+ *
3150
+ * **本层不缓存包体、也不解码**:它是一条分配的内容,不是「服务端此刻认什么」的投影;
3151
+ * 缓存下来等于把每次同步装过的所有技能包都钉在内存里(base64 后每条约 5 MiB)。
3152
+ * 校验与落盘在技能域(spec §4.6)。
3153
+ */
3154
+ async package(assignmentId, signal) {
3155
+ const raw = await this.rpc.call(MANAGED_SKILLS_CHANNEL, "package", { assignmentId }, signal);
3156
+ const value = unwrapManagedSkillsResult(raw);
3157
+ if (typeof value !== "object" || value === null) {
3158
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE);
3159
+ }
3160
+ const record = value;
3161
+ const id = text2(record["assignmentId"]);
3162
+ const skillName = text2(record["skillName"]);
3163
+ const contentSha256 = text2(record["contentSha256"]);
3164
+ const archiveBase64 = text2(record["archiveBase64"]);
3165
+ if (id === void 0 || skillName === void 0 || contentSha256 === void 0 || archiveBase64 === void 0) {
3166
+ throw new ManagedSkillsRpcError("internal", INVALID_RESPONSE_MESSAGE);
3167
+ }
3168
+ return { assignmentId: id, skillName, contentSha256, archiveBase64 };
3169
+ }
3170
+ /** 订阅镜像变化(技能域据此重渲染 / 重排同步)。返回退订函数。 */
3171
+ subscribe(listener) {
3172
+ this.listeners.add(listener);
3173
+ return () => {
3174
+ this.listeners.delete(listener);
3175
+ };
3176
+ }
3177
+ /**
3178
+ * 清空镜像(登出 / 换账号:清单是按人的,留着会串号),并**作废在飞的请求**。
3179
+ *
3180
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的,它的
3181
+ * 清单属于上一个人——清空之后到来的调用者若复用了它,就会拿旧账号的清单去对账本地受管
3182
+ * 技能,把新账号被分配的技能全删掉(`refresh` 的返回值是唯一被允许用来删的那条路,
3183
+ * spec §6)。所以世代 +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。
3184
+ *
3185
+ * 用世代号而**不是**把 `inFlight` 直接抹成 undefined:旧 promise 的 `finally` 会回头把
3186
+ * **新**发起的那条记账清掉,于是「清空后重新发起」与「又在飞的旧请求」互相踩。世代判据
3187
+ * 天然避开这一格——收尾只在「自己还是账上那一条」时才发生(与 Host 侧 `src/managed-skills.ts`
3188
+ * 的 `load()` 同形)。
3189
+ *
3190
+ * ⚠️ 这条修复**刻意只落在技能这半**:`src/client/managed-mcp.ts` 是逐字同形的既有缺口,
3191
+ * **用户已裁定 MCP 那半本轮不动**。别为了「两半对齐」把这里改回原样。
3192
+ */
3193
+ clear() {
3194
+ this.generation += 1;
3195
+ this.current = void 0;
3196
+ this.emit();
3197
+ }
3198
+ /**
3199
+ * 并发去重:`warm` 与 `refresh` 撞在一起(启动同步与定时同步常常同拍)时共用一次
3200
+ * 请求,且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
3201
+ *
3202
+ * 合并只会让清单更旧(`warm` 可能命中 Host 缓存)、**不会让它更空**:旧清单里条目更多,
3203
+ * 于是删除得更少——偏差落在「该删的这次没删」这一侧,正是 spec §6 允许的那一侧。
3204
+ *
3205
+ * 上面这条讲的是**新旧次序**,管不到**身份**:并发合并还有第二个维度——「这几次调用是不是
3206
+ * 同一个人的」。世代号({@link clear})负责的就是这一格:换账号之后账上那条属于上一个人,
3207
+ * 谁都不许复用,必须各自重新发起。
3208
+ *
3209
+ * 传给 `task` 的 `generation` 是**发起这一次请求时**的世代号,由它决定这次结果能不能写进镜像
3210
+ * (见 {@link adopt})——晚到的旧世代结果照旧返回给它自己的调用方,但不许覆盖新世代的镜像。
3211
+ */
3212
+ async dedupe(task) {
3213
+ const inFlight = this.inFlight;
3214
+ if (inFlight !== void 0 && inFlight.generation === this.generation) return await inFlight.task;
3215
+ const generation = this.generation;
3216
+ const promise = task(generation);
3217
+ this.inFlight = { generation, task: promise };
3218
+ try {
3219
+ return await promise;
3220
+ } finally {
3221
+ if (this.inFlight?.task === promise) this.inFlight = void 0;
3222
+ }
3223
+ }
3224
+ /**
3225
+ * 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。
3226
+ *
3227
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
3228
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的清单占住」,而换账号时下一次同步
3229
+ * 就靠 `snapshot()` / `isStale()` 判「要不要拉」——镜像被别人的清单占着,这一页会既不显示
3230
+ * 新账号的技能,也不触发同步。
3231
+ */
3232
+ adopt(value, generation) {
3233
+ const snapshot = toSnapshot2(value);
3234
+ if (generation !== this.generation) return snapshot;
3235
+ this.current = snapshot;
3236
+ this.emit();
3237
+ return snapshot;
3238
+ }
3239
+ emit() {
3240
+ for (const listener of [...this.listeners]) listener();
3241
+ }
3242
+ };
3243
+
3244
+ // src/client/managed-workbench.ts
3245
+ var MANAGED_WORKBENCH_CHANNEL = "/hiwork-workbench";
3246
+ var MANAGED_WORKBENCH_STALE_MS = 5 * 6e4;
3247
+ var ManagedWorkbenchRpcError = class extends Error {
3248
+ constructor(code, message) {
3249
+ super(message);
3250
+ this.code = code;
3251
+ this.name = "ManagedWorkbenchRpcError";
3252
+ }
3253
+ };
3254
+ var GENERIC_FAILURE_MESSAGE2 = "\u5DE5\u4F5C\u53F0\u5E94\u7528\u6E05\u5355\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
3255
+ var INVALID_RESPONSE_MESSAGE2 = "\u5DE5\u4F5C\u53F0\u6E05\u5355\u4E3B\u673A\u8FD4\u56DE\u4E86\u65E0\u6548\u54CD\u5E94\u3002";
3256
+ function unwrapManagedWorkbenchResult(value) {
3257
+ if (typeof value !== "object" || value === null || !("ok" in value)) {
3258
+ throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3259
+ }
3260
+ const result = value;
3261
+ if (result.ok === true) return result.value;
3262
+ if (result.ok === false) {
3263
+ const code = typeof result.error?.code === "string" ? result.error.code : "internal";
3264
+ const message = typeof result.error?.message === "string" && result.error.message.trim() !== "" ? result.error.message : GENERIC_FAILURE_MESSAGE2;
3265
+ throw new ManagedWorkbenchRpcError(code, message);
3266
+ }
3267
+ throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3268
+ }
3269
+ function text3(value) {
3270
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
3271
+ }
3272
+ function isRecord(value) {
3273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3274
+ }
3275
+ function stringArray(value) {
3276
+ if (!Array.isArray(value)) return [];
3277
+ return value.filter((item) => typeof item === "string");
3278
+ }
3279
+ function toApp(value) {
3280
+ if (!isRecord(value)) return void 0;
3281
+ const code = text3(value["code"]);
3282
+ const executor = value["executor"];
3283
+ if (code === void 0 || !isRecord(executor)) return void 0;
3284
+ const kind = text3(executor["kind"]);
3285
+ if (kind === void 0) return void 0;
3286
+ const normalizedExecutor = { ...executor, kind };
3287
+ const mode = text3(executor["mode"]);
3288
+ if (mode === void 0) delete normalizedExecutor.mode;
3289
+ else normalizedExecutor.mode = mode;
3290
+ const category = isRecord(value["category"]) ? value["category"] : {};
3291
+ const inputs = [];
3292
+ for (const item of Array.isArray(value["inputs"]) ? value["inputs"] : []) {
3293
+ if (!isRecord(item)) continue;
3294
+ const key = text3(item["key"]);
3295
+ if (key === void 0) continue;
3296
+ inputs.push({
3297
+ ...item,
3298
+ key,
3299
+ type: text3(item["type"]) ?? "",
3300
+ label: text3(item["label"]) ?? key,
3301
+ required: item["required"] === true
3302
+ });
3303
+ }
3304
+ const outputs = [];
3305
+ for (const item of Array.isArray(value["outputs"]) ? value["outputs"] : []) {
3306
+ if (!isRecord(item)) continue;
3307
+ const key = text3(item["key"]);
3308
+ if (key === void 0) continue;
3309
+ outputs.push({ ...item, key, type: text3(item["type"]) ?? "" });
3310
+ }
3311
+ const iconUrl = text3(value["iconUrl"]);
3312
+ const revision = value["revision"];
3313
+ return {
3314
+ code,
3315
+ displayName: text3(value["displayName"]) ?? code,
3316
+ summary: text3(value["summary"]) ?? "",
3317
+ ...iconUrl === void 0 ? {} : { iconUrl },
3318
+ category: { code: text3(category["code"]) ?? "", name: text3(category["name"]) ?? "" },
3319
+ tags: stringArray(value["tags"]),
3320
+ featured: value["featured"] === true,
3321
+ revision: typeof revision === "number" && Number.isFinite(revision) ? revision : 0,
3322
+ executor: normalizedExecutor,
3323
+ inputs,
3324
+ outputs,
3325
+ effects: stringArray(value["effects"])
3326
+ };
3327
+ }
3328
+ function toSnapshot3(value) {
3329
+ if (!isRecord(value)) throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3330
+ const rawApps = value["apps"];
3331
+ if (!Array.isArray(rawApps)) throw new ManagedWorkbenchRpcError("internal", INVALID_RESPONSE_MESSAGE2);
3332
+ const apps = [];
3333
+ for (const item of rawApps) {
3334
+ const app = toApp(item);
3335
+ if (app !== void 0) apps.push(app);
3336
+ }
3337
+ const snapshot = {
3338
+ revision: typeof value["revision"] === "number" && Number.isFinite(value["revision"]) ? value["revision"] : 0,
3339
+ fetchedAt: typeof value["fetchedAt"] === "string" ? value["fetchedAt"] : "",
3340
+ apps,
3341
+ stale: value["stale"] === true
3342
+ };
3343
+ const lastError = value["lastError"];
3344
+ if (isRecord(lastError)) {
3345
+ snapshot.lastError = {
3346
+ code: text3(lastError["code"]) ?? "internal",
3347
+ message: typeof lastError["message"] === "string" ? lastError["message"] : GENERIC_FAILURE_MESSAGE2
3348
+ };
3349
+ }
3350
+ return snapshot;
3351
+ }
3352
+ var ManagedWorkbenchStore = class {
3353
+ constructor(rpc) {
3354
+ this.rpc = rpc;
3355
+ }
3356
+ current;
3357
+ listeners = /* @__PURE__ */ new Set();
3358
+ /**
3359
+ * 在飞请求**连同它所属的世代**一起记账:只有同一世代的那条才可以被复用。
3360
+ *
3361
+ * 光记 promise 不够——`clear()`(登出 / 换账号)之后,账上那条是**上一个人**发起的,它的清单
3362
+ * 属于上一个人。复用它等于让新账号的货架显示旧账号的应用(管理员给谁分配是按人的)。
3363
+ * 所以要有一个「这条账还算不算数」的判据,见 {@link clear}。
3364
+ */
3365
+ inFlight;
3366
+ /** 世代号:`clear()` 每调用一次 +1,用来作废它之前发起的在飞请求。 */
3367
+ generation = 0;
3368
+ /** 本地镜像(同步)。从没成功同步过返回 undefined。**不要拿它做下架决策**(见文件头)。 */
3369
+ snapshot() {
3370
+ return this.current;
3371
+ }
3372
+ /** 本地镜像是否已经陈旧(或从未同步过)。 */
3373
+ isStale(ttlMs = MANAGED_WORKBENCH_STALE_MS, now = Date.now()) {
3374
+ if (this.current === void 0 || this.current.fetchedAt === "") return true;
3375
+ const fetchedAt = Date.parse(this.current.fetchedAt);
3376
+ if (Number.isNaN(fetchedAt)) return true;
3377
+ return now - fetchedAt >= ttlMs;
3378
+ }
3379
+ /**
3380
+ * 强制同步一次并返回**刚拉到的**清单。
3381
+ *
3382
+ * 返回值(而不是让人随后去读 `snapshot()`)是刻意的:只有这个返回值保证「这一次网络请求成功了」,
3383
+ * 用它判断「这个应用还在不在」才是安全的;读镜像可能读到上一次的结果。失败时抛错且**不动镜像**
3384
+ * ——调用方据此保持货架原样(离线态显示上次成功缓存 + 「可能不是最新」)。
3385
+ */
3386
+ async refresh(signal) {
3387
+ return await this.dedupe(async (generation) => {
3388
+ const raw = await this.rpc.call(MANAGED_WORKBENCH_CHANNEL, "refresh", {}, signal);
3389
+ return this.adopt(unwrapManagedWorkbenchResult(raw), generation);
3390
+ });
3391
+ }
3392
+ /**
3393
+ * 取一份清单,**能用 Host 的预热缓存就用**(Host 后台每 30 分钟拉一次)。
3394
+ *
3395
+ * 与 {@link refresh} 的分工:这里是「挂载时先把界面填上」的便宜入口——命中缓存时一次网络请求
3396
+ * 都不发;Host 缓存也是冷的才会真的去拉。返回值仍保证是「某一次成功拉取的结果」,但**可能比
3397
+ * 服务端现状旧**,所以调用方按 `stale` / `isStale()` 判定它够不够权威,不够就再 `refresh()`。
3398
+ */
3399
+ async warm(signal) {
3400
+ return await this.dedupe(async (generation) => {
3401
+ const raw = await this.rpc.call(MANAGED_WORKBENCH_CHANNEL, "snapshot", {}, signal);
3402
+ return this.adopt(unwrapManagedWorkbenchResult(raw), generation);
3403
+ });
3404
+ }
3405
+ /** 订阅镜像变化(货架页据此重渲染)。返回退订函数。 */
3406
+ subscribe(listener) {
3407
+ this.listeners.add(listener);
3408
+ return () => {
3409
+ this.listeners.delete(listener);
3410
+ };
3411
+ }
3412
+ /**
3413
+ * 清空镜像(登出 / 换账号:清单是按人的,留着会串号),并**作废在飞的请求**。
3414
+ *
3415
+ * 只清镜像是不够的:清空那一刻正有一次拉取在飞时,那次拉取是**上一个人**发起的,它的清单
3416
+ * 属于上一个人——清空之后到来的调用者若复用了它,新账号的货架就会显示旧账号的应用。所以
3417
+ * 世代 +1:那之后的调用者一律不复用旧世代的那条,自己重新发一次 RPC。
3418
+ *
3419
+ * 用世代号而**不是**把 `inFlight` 直接抹成 undefined:旧 promise 的 `finally` 会回头把**新**
3420
+ * 发起的那条记账清掉,于是「清空后重新发起」与「又在飞的旧请求」互相踩。世代判据天然避开
3421
+ * 这一格——收尾只在「自己还是账上那一条」时才发生(与 Host 侧 `src/workbench-bridge.ts`
3422
+ * 的 `load()` 同形,也与 `src/client/managed-skills.ts` 的同名方法逐字同款)。
3423
+ */
3424
+ clear() {
3425
+ this.generation += 1;
3426
+ this.current = void 0;
3427
+ this.emit();
3428
+ }
3429
+ /**
3430
+ * 并发去重:`warm` 与 `refresh` 撞在一起(挂载预热与手动刷新常常同拍)时共用一次请求,
3431
+ * 且**共用同一个返回值**——两边对「这一次拉到了什么」的判断必须一致。
3432
+ *
3433
+ * 合并只会让清单更旧(`warm` 可能命中 Host 缓存)、**不会让它更空**:旧清单里条目更多,
3434
+ * 于是「该消失的这次没消失」——偏差落在保守的那一侧。
3435
+ *
3436
+ * 上面这条讲的是**新旧次序**,管不到**身份**:世代号({@link clear})负责的就是这一格——
3437
+ * 换账号之后账上那条属于上一个人,谁都不许复用,必须各自重新发起。
3438
+ */
3439
+ async dedupe(task) {
3440
+ const inFlight = this.inFlight;
3441
+ if (inFlight !== void 0 && inFlight.generation === this.generation) return await inFlight.task;
3442
+ const generation = this.generation;
3443
+ const promise = task(generation);
3444
+ this.inFlight = { generation, task: promise };
3445
+ try {
3446
+ return await promise;
3447
+ } finally {
3448
+ if (this.inFlight?.task === promise) this.inFlight = void 0;
3449
+ }
3450
+ }
3451
+ /**
3452
+ * 采纳一份成功响应:写镜像 + 通知订阅者(失败路径永远不会走到这里)。
3453
+ *
3454
+ * 世代不符的那一份**不写镜像**(但仍原样返回给调用方):它是 `clear()` 之前发起的请求,
3455
+ * 属于上一个人。允许它写回等于「清空之后又被上一个人的清单占住」,而换账号时下一次同步就靠
3456
+ * `snapshot()` / `isStale()` 判「要不要拉」——镜像被别人的清单占着,这一页会既不显示新账号的
3457
+ * 应用,也不触发同步。
3458
+ */
3459
+ adopt(value, generation) {
3460
+ const snapshot = toSnapshot3(value);
3461
+ if (generation !== this.generation) return snapshot;
3462
+ this.current = snapshot;
3463
+ this.emit();
3464
+ return snapshot;
3465
+ }
3466
+ emit() {
3467
+ for (const listener of [...this.listeners]) listener();
3468
+ }
3469
+ };
3470
+
3007
3471
  // src/client/index.ts
3008
3472
  var inject = ["slots", "locale", "remote", "remote.credentials", "settingsScope", "connection"];
3009
3473
  var INTERACTION_STYLE_ID = "hiwork-core-interaction-styles";
@@ -3012,7 +3476,7 @@ function injectInteractionStyles() {
3012
3476
  if (document.getElementById(INTERACTION_STYLE_ID) !== null) return;
3013
3477
  const style = document.createElement("style");
3014
3478
  style.id = INTERACTION_STYLE_ID;
3015
- style.textContent = [interactionCss, brandCss, frameCss, hostHideCss].join("\n");
3479
+ style.textContent = [interactionCss, brandCss, frameCss, hostHideCss, composerCss].join("\n");
3016
3480
  document.head.appendChild(style);
3017
3481
  }
3018
3482
  function apply(ctx) {
@@ -3042,6 +3506,8 @@ function apply(ctx) {
3042
3506
  const sessionCost = new SessionCostStore(connection.rpc);
3043
3507
  const dailyQuotes = new DailyDataStore(connection.rpc);
3044
3508
  const managedMcp = new ManagedMcpStore(connection.rpc);
3509
+ const managedSkills = new ManagedSkillsStore(connection.rpc);
3510
+ const managedWorkbench = new ManagedWorkbenchStore(connection.rpc);
3045
3511
  const appVersion = createAppVersion(connection.rpc);
3046
3512
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3047
3513
  name: "settings.section",
@@ -3060,6 +3526,8 @@ function apply(ctx) {
3060
3526
  usage.clear();
3061
3527
  sessionCost.clear();
3062
3528
  managedMcp.clear();
3529
+ managedSkills.clear();
3530
+ managedWorkbench.clear();
3063
3531
  }), "hiwork-core: \u7528\u91CF\u7F13\u5B58\u968F\u8D26\u53F7 / Key \u53D8\u5316\u6E05\u7A7A");
3064
3532
  ctx.effect(() => observeAboutNavIcon(), "hiwork-core: about nav icon");
3065
3533
  ctx.slots.inject("sidebar.brand.mark", () => ctx.slots.register({
@@ -3078,9 +3546,9 @@ function apply(ctx) {
3078
3546
  }, UserBadge));
3079
3547
  ctx.effect(() => mountLoginGate(scope, storeCredential), "hiwork-core: login gate");
3080
3548
  ctx.effect(() => observeHeroDecorations(document, dailyQuotes), "hiwork-core: hero decorations");
3081
- installFeatureCenter(ctx, managedMcp);
3549
+ installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench);
3082
3550
  }
3083
- function installFeatureCenter(ctx, managedMcp) {
3551
+ function installFeatureCenter(ctx, managedMcp, managedSkills, managedWorkbench) {
3084
3552
  const featureRegistry = createFeatureRegistry();
3085
3553
  let center;
3086
3554
  const sessions = ctx.get("sessions");
@@ -3109,6 +3577,27 @@ function installFeatureCenter(ctx, managedMcp) {
3109
3577
  refresh: async (signal) => await managedMcp.refresh(signal),
3110
3578
  assertion: async (assignmentId, signal) => await managedMcp.assertion(assignmentId, signal),
3111
3579
  subscribe: (listener) => managedMcp.subscribe(listener)
3580
+ },
3581
+ // 受管技能面:技能域(hiwork-capabilities)按这个成员判断「有没有这个能力」,老 core 上
3582
+ // 它是 undefined,能力中心照常工作、只是没有受管技能那一块(可选解耦,见 contracts.ts)。
3583
+ // 六项与 spec §4.5 逐字对应;核心不落盘、不解包、不持有会话 cookie——包体由 Host 取。
3584
+ managedSkills: {
3585
+ snapshot: () => managedSkills.snapshot(),
3586
+ isStale: (ttlMs) => managedSkills.isStale(ttlMs),
3587
+ warm: async (signal) => await managedSkills.warm(signal),
3588
+ refresh: async (signal) => await managedSkills.refresh(signal),
3589
+ package: async (assignmentId, signal) => await managedSkills.package(assignmentId, signal),
3590
+ subscribe: (listener) => managedSkills.subscribe(listener)
3591
+ },
3592
+ // 工作台应用清单面:工作台域(hiwork-workbench)按这个成员判断「有没有这个能力」,老 core 上
3593
+ // 它是 undefined,货架按「本部署尚未启用工作台」降级(spec §10 的降级矩阵)。M0 只有清单
3594
+ // 相关的方法——`submit` / `poll` / `cancel` / `materialize` 属 M1(见 contracts.ts)。
3595
+ managedWorkbench: {
3596
+ snapshot: () => managedWorkbench.snapshot(),
3597
+ isStale: (ttlMs) => managedWorkbench.isStale(ttlMs),
3598
+ warm: async (signal) => await managedWorkbench.warm(signal),
3599
+ refresh: async (signal) => await managedWorkbench.refresh(signal),
3600
+ subscribe: (listener) => managedWorkbench.subscribe(listener)
3112
3601
  }
3113
3602
  };
3114
3603
  ctx.provide(HIWORK_FEATURE_CENTER_SERVICE, face);