dsh-agentone 0.5.18 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,10 @@ lark-cli / ccpg-cli 状态探测。
13
13
  OpenAI 兼容代理(`/api/plugin/v1`),模型密钥不下发到本机;对话经平台
14
14
  走 TokenHub 按用户计量。平台 403 的 `plan_required` 错误码原样透传,
15
15
  前端据此引导申请套餐(不依赖中文文案匹配)。
16
- - **SkillHub 技能(M3)**:套餐技能启动/登录时自动装齐;技能市场搜索、
17
- 手动安装/卸载;已装技能平台侧对比 latest 版本/指纹,有更新时展示
16
+ - **SkillHub 技能(M3)**:套餐技能自动装齐(启动/登录时,且常驻期间
17
+ 套餐生效、套餐新配技能时由前端触发器与手动「同步套餐技能」按钮补装);
18
+ 技能市场搜索、手动安装/卸载,装/卸载后技能列表即时刷新(失效 45s
19
+ status 缓存);已装技能平台侧对比 latest 版本/指纹,有更新时展示
18
20
  「v旧 → v新」并支持一键升级(覆盖式重装,原子替换旧目录);技能包
19
21
  zip 走平台代理下载(穿越清洗 + 原子落盘)。
20
22
  - **SkillHub 连接配置(M3+)**:登录后自动从平台拉取 registry 与用户级
package/lib/index.js CHANGED
@@ -628,6 +628,12 @@ async function syncPlanSkills(config, log, credentials) {
628
628
  const STATUS_CACHE_TTL_MS = 45_000;
629
629
  let statusCache = { at: 0, key: null, skills: null, displayNames: null };
630
630
 
631
+ /** 失效技能列表缓存:安装/卸载/同步改动台账后调用,让下一次 status 立即
632
+ * 拉到新列表(前端装完按钮的 refresh() 依赖这点,否则最长 45s 显示旧状态)。 */
633
+ function invalidateStatusCache() {
634
+ statusCache = { at: 0, key: statusCache.key, skills: null, displayNames: null };
635
+ }
636
+
631
637
  async function cachedPlatformStatus(config, credentials) {
632
638
  const capabilities = await fetchPlatformCapabilities(config.platformUrl, credentials.access_token);
633
639
  const key = `${config.platformUrl}|${credentials.access_token}`;
@@ -801,8 +807,22 @@ export function apply(ctx, config) {
801
807
  // 失败自愈:模型同步残留错误时由轮询驱动后台重试(成功即清,
802
808
  // 下次轮询状态页恢复);错误已被登录/手动同步清除时取消待定
803
809
  // 重试并复位退避计数。
804
- if (credentials.model_error) scheduleModelRetry(config, log);
805
- else {
810
+ if (credentials.model_error) {
811
+ // 套餐已生效(active)却残留 model_error:多半是审批前同步失败
812
+ // 的旧错误(plan_required),此时重试必然成功——立即同步一次,
813
+ // 不走首轮 60s 的指数退避。常量幂等:同步成功清掉错误后不再进。
814
+ const planActive = capabilities?.plan_state === 'active';
815
+ if (planActive) {
816
+ syncPlatformModel(config, log)
817
+ .then(() => {
818
+ clearModelRetry();
819
+ modelRetryAttempts = 0;
820
+ })
821
+ .catch(() => scheduleModelRetry(config, log));
822
+ } else {
823
+ scheduleModelRetry(config, log);
824
+ }
825
+ } else {
806
826
  clearModelRetry();
807
827
  modelRetryAttempts = 0;
808
828
  }
@@ -984,12 +1004,35 @@ export function apply(ctx, config) {
984
1004
  slug,
985
1005
  { config },
986
1006
  );
1007
+ invalidateStatusCache();
987
1008
  sendJson(response, 200, { ok: true, ...result });
988
1009
  } catch (error) {
989
1010
  sendJson(response, 502, { error: error?.message || '技能安装失败' });
990
1011
  }
991
1012
  });
992
1013
 
1014
+ // 套餐技能同步:把平台上 source=plan 且未安装的技能装齐。前端在套餐
1015
+ // 生效联动与自动补装触发器里调,也可手动触发;同步结果失效技能缓存。
1016
+ register('/agentone/api/skill/sync', async (request, response) => {
1017
+ if (request.method !== 'POST') {
1018
+ response.writeHead(405, { allow: 'POST' });
1019
+ response.end();
1020
+ return;
1021
+ }
1022
+ try {
1023
+ const credentials = await ensureFreshToken(config, log);
1024
+ if (!credentials) {
1025
+ sendJson(response, 401, { error: '请先完成飞书登录' });
1026
+ return;
1027
+ }
1028
+ const result = await syncPlanSkills(config, log, credentials);
1029
+ invalidateStatusCache();
1030
+ sendJson(response, 200, { ok: true, installed: result?.installed ?? 0, failed: result?.failed ?? 0 });
1031
+ } catch (error) {
1032
+ sendJson(response, 502, { error: error?.message || '同步套餐技能失败' });
1033
+ }
1034
+ });
1035
+
993
1036
  register('/agentone/api/skill/catalog', async (request, response) => {
994
1037
  if (request.method !== 'GET') {
995
1038
  response.writeHead(405, { allow: 'GET' });
@@ -1043,6 +1086,7 @@ export function apply(ctx, config) {
1043
1086
  return;
1044
1087
  }
1045
1088
  await removePlatformSkill(config.platformUrl, credentials.access_token, slug, { config });
1089
+ invalidateStatusCache();
1046
1090
  sendJson(response, 200, { ok: true, canonical_slug: slug });
1047
1091
  } catch (error) {
1048
1092
  sendJson(response, 502, { error: error?.message || '技能卸载失败' });
package/lib/page.js CHANGED
@@ -230,6 +230,33 @@ $('cli-recheck-btn').onclick = () => {
230
230
  document.querySelectorAll('.tab').forEach((tab) => {
231
231
  tab.onclick = () => activateTab(tab.dataset.tab);
232
232
  });
233
+ // 套餐技能自动补装触发器:forced=true(套餐刚生效)直接触发;否则仅当
234
+ // status 列表里有未安装的套餐技能才触发。60s 节流 + in-flight 单飞,
235
+ // 失败靠下一次轮询自然重试(skills_error 由 status 带回展示)。
236
+ let planSkillSyncAt = 0;
237
+ let planSkillSyncing = false;
238
+ const maybeAutoSyncPlanSkills = (forced) => {
239
+ if (planSkillSyncing) return;
240
+ const now = Date.now();
241
+ if (now - planSkillSyncAt < 60000) return;
242
+ const skills = lastStatus?.skills?.skills;
243
+ if (!forced) {
244
+ if (!Array.isArray(skills)) return;
245
+ if (!skills.some((skill) => skill.source === 'plan' && !skill.installed)) return;
246
+ }
247
+ planSkillSyncAt = now;
248
+ planSkillSyncing = true;
249
+ fetch('api/skill/sync', { method: 'POST' })
250
+ .catch(() => { planSkillSyncAt = 0; })
251
+ .finally(() => { planSkillSyncing = false; });
252
+ };
253
+ // 套餐技能同步:调后端 /skill/sync 装齐 source=plan 且未安装的技能。
254
+ // 三个调用方:套餐生效联动、自动补装触发器、技能页手动按钮。
255
+ const syncPlanSkills = (btn) => withLoading(
256
+ btn,
257
+ () => fetch('api/skill/sync', { method: 'POST' }),
258
+ null,
259
+ );
233
260
  const skillAction = (slug, action, btn, successText) => withLoading(
234
261
  btn,
235
262
  () => fetch('api/skill/' + action, {
@@ -849,15 +876,20 @@ const refresh = async () => {
849
876
  }
850
877
  lastStatus = s;
851
878
  const modelEnabled = s.capabilities?.channels?.model?.enabled !== false;
852
- // 套餐审批通过检测:plan_state 由非 active 变为 active 时自动同步模型
879
+ const skillhubEnabled = s.capabilities?.channels?.skillhub?.enabled !== false;
880
+ // 套餐审批通过检测:plan_state 由非 active 变为 active 时自动同步模型与技能
853
881
  const planState = s.capabilities?.plan_state;
854
882
  if (s.logged_in && modelEnabled) {
855
883
  if (planState === 'active' && lastPlanState && lastPlanState !== 'active') {
856
884
  notify('套餐已生效,正在同步模型…');
857
885
  fetch('api/model/sync', { method: 'POST' }).catch(() => {});
886
+ if (skillhubEnabled) maybeAutoSyncPlanSkills(true);
858
887
  }
859
888
  lastPlanState = planState || null;
860
889
  }
890
+ // 常驻期间自动补装:列表出现「套餐技能未安装」(管理员后配、启动时同步
891
+ // 瞬时失败)就触发一次同步;节流 + 单飞,装完列表变 installed 自然停
892
+ if (s.logged_in && skillhubEnabled) maybeAutoSyncPlanSkills(false);
861
893
  statePill.className = 'pill ' + (s.logged_in ? 'ok' : 'warn');
862
894
  statePill.textContent = s.logged_in ? '已登录' : '未登录';
863
895
  // 首个状态到达:骨架占位让位给登录卡/主内容
@@ -892,11 +924,21 @@ const refresh = async () => {
892
924
  }
893
925
  $('model-body').innerHTML = lines.join('')
894
926
  + (planBlocked
895
- ? '<div class="row" style="margin-top:10px"><button id="goto-plan-btn" class="btn primary small">去申请套餐</button></div>'
927
+ ? '<div class="row" style="margin-top:10px">'
928
+ + '<button id="retry-model-btn" class="btn ghost small" data-loading="同步中…">重新同步模型</button>'
929
+ + '<button id="goto-plan-btn" class="btn primary small">去申请套餐</button></div>'
896
930
  : '');
897
931
  $('sync-btn').hidden = !modelEnabled || planBlocked;
898
932
  const gotoPlan = $('goto-plan-btn');
899
933
  if (gotoPlan) gotoPlan.onclick = () => activateTab('plan');
934
+ // planBlocked 兜底手动出口:旧错误残留时(套餐已批但本地还挂着
935
+ // plan_required)可立即重同步,不等后端自愈的退避周期
936
+ const retryModel = $('retry-model-btn');
937
+ if (retryModel) retryModel.onclick = () => withLoading(
938
+ retryModel,
939
+ () => fetch('api/model/sync', { method: 'POST' }),
940
+ '平台模型已同步',
941
+ );
900
942
  }
901
943
  if (s.logged_in && s.skillhub) {
902
944
  const sh = s.skillhub;
@@ -944,6 +986,20 @@ $('skillhub-sync-btn').onclick = () => withLoading(
944
986
  () => fetch('api/skillhub/sync', { method: 'POST' }),
945
987
  'SkillHub 配置已同步',
946
988
  );
989
+ // 手动同步套餐技能:文案按结果给(装了几个),失败由 withLoading 展示
990
+ $('skill-sync-btn').onclick = () => withLoading(
991
+ $('skill-sync-btn'),
992
+ async () => {
993
+ const resp = await fetch('api/skill/sync', { method: 'POST' });
994
+ const data = await resp.clone().json().catch(() => ({}));
995
+ if (resp.ok) {
996
+ const installed = Number(data.installed) || 0;
997
+ notify(installed > 0 ? '已同步,新装 ' + installed + ' 个套餐技能' : '套餐技能已是最新');
998
+ }
999
+ return resp;
1000
+ },
1001
+ null,
1002
+ );
947
1003
  logoutBtn.onclick = () => withLoading(
948
1004
  logoutBtn,
949
1005
  () => fetch('api/logout', { method: 'POST' }),
@@ -1033,6 +1089,9 @@ export function statusPageHtml() {
1033
1089
  <h3>平台技能 <span class="muted">套餐自动同步</span></h3>
1034
1090
  <div id="skills-list"></div>
1035
1091
  <div class="hint" id="skills-meta"></div>
1092
+ <div class="row" style="margin-top:8px">
1093
+ <button id="skill-sync-btn" class="btn ghost small" data-loading="同步中…">同步套餐技能</button>
1094
+ </div>
1036
1095
  <div class="search" style="margin-top:12px">
1037
1096
  <input id="search-input" placeholder="搜索技能市场,如:数据 / 报告 / lark" />
1038
1097
  <button id="search-btn" class="btn ghost small">搜索</button>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "0.5.18",
4
- "description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(含升级检测与一键升级)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
3
+ "version": "0.5.20",
4
+ "description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(升级检测一键升级、装后即时刷新、套餐技能自动同步)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {