dsh-agentone 0.5.37 → 0.5.38

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
@@ -1,14 +1,15 @@
1
1
  # dsh-agentone
2
2
 
3
3
  AgentOne 平台集成插件(dsh web 版 / 桌面版通用)。安装后让独立 dsh 接入公司
4
- AgentOne 平台:飞书登录、平台模型、SkillHub 技能、套餐申请、dsh 插件管理、
4
+ AgentOne 平台:IDOne 登录、平台模型、SkillHub 技能、套餐申请、dsh 插件管理、
5
5
  lark-cli / ccpg-cli 状态探测。
6
6
 
7
7
  ## 能力总览(M1–M4 已全部交付)
8
8
 
9
- - **飞书登录(M1)**:设备登录流(一次性授权码),凭据保存在本机
10
- `$DSH_HOME/agentone/credentials.json`(0600)。access 令牌临期自动轮换
11
- (single-flight),refresh 失效自动回到未登录态。
9
+ - **平台登录(M1)**:设备登录流(一次性授权码),入口按平台能力面自动
10
+ 路由——平台开启邮箱锚点模式(IDOne 统一登录)时走 IDOne,否则回退
11
+ 飞书;凭据保存在本机 `$DSH_HOME/agentone/credentials.json`(0600)。
12
+ access 令牌临期自动轮换(single-flight),refresh 失效自动回到未登录态。
12
13
  - **平台模型(M2)**:登录后自动把 dsh 的 llm provider 指向 AgentOne
13
14
  OpenAI 兼容代理(`/api/plugin/v1`),模型密钥不下发到本机;对话经平台
14
15
  走 TokenHub 按用户计量。平台 403 的 `plan_required` 错误码原样透传,
@@ -46,7 +47,7 @@ lark-cli / ccpg-cli 状态探测。
46
47
  每个会话基线加载,project 级指令优先);幂等不重复注入,用户已有语言
47
48
  偏好或删除标记块后不再介入(本机只做一次决策,记录于
48
49
  `$DSH_HOME/agentone/language-default.json`)。
49
- - **首启引导**:dsh 首次启动弹出飞书登录引导(order -50,早于官方
50
+ - **首启引导**:dsh 首次启动弹出平台登录引导(order -50,早于官方
50
51
  「添加 API Key」),登录一次即跳过官方密钥引导;暗色主题适配。
51
52
 
52
53
  所有平台请求统一走 `lib/http.js`(默认 10s 超时、技能包下载 60s),
@@ -63,8 +64,9 @@ dsh plugin --profile web add dsh-agentone
63
64
 
64
65
  ## 使用
65
66
 
66
- 打开 dsh web 的 `/agentone/` 页面(或 dsh 设置 → AgentOne)→「飞书登录」→
67
- 浏览器完成平台飞书 OAuth → 自动跳回并显示已登录账号。授权完成页会等待
67
+ 打开 dsh web 的 `/agentone/` 页面(或 dsh 设置 → AgentOne)→「IDOne 登录」
68
+ (平台未开启统一登录时为「飞书登录」)→ 浏览器完成平台 OAuth → 自动跳回
69
+ 并显示已登录账号。授权完成页会等待
68
70
  模型配置就绪后自动关闭;配置失败时展示原因并提供「重试同步」。
69
71
 
70
72
  登录成功后插件自动完成模型配置(也可点「重新同步模型」手动触发):
@@ -88,8 +90,8 @@ dsh plugin --profile web add dsh-agentone
88
90
 
89
91
  ```
90
92
  插件页 ──POST /agentone/api/login──▶ 本插件后端
91
- ◀─ {login_url} ──
92
- 浏览器 ──login_url──▶ 平台飞书 OAuth ──▶ /api/auth/plugin/landing
93
+ ◀─ {login_url, provider} ──
94
+ 浏览器 ──login_url──▶ 平台 OAuth(IDOne 优先,飞书兜底)──▶ /api/auth/plugin/landing
93
95
  landing(JS, Bearer) ──POST /api/auth/plugin/grant──▶ {redirect_url(一次性 code)}
94
96
  浏览器 ──redirect_url──▶ 本插件 /agentone/callback?code
95
97
  插件后端 ──POST /api/auth/plugin/exchange──▶ {access_token}
package/lib/auth.js CHANGED
@@ -1,10 +1,12 @@
1
1
  // AgentOne 设备登录流(一次性授权码)。
2
2
  //
3
3
  // 流程(与平台 app/api/plugin_auth.py 契约对应):
4
- // 1. buildLoginUrl:拼平台飞书 OAuth 入口,redirect 指向平台 landing 页并
5
- // 携带 callback_url/state(经飞书 OAuth state 往返透传);
6
- // 2. 浏览器完成飞书登录 → landing 页用平台令牌换一次性 code → 跳转
7
- // callback_url(即本插件的 /agentone/callback 路由);
4
+ // 1. buildLoginUrl:拼平台 OAuth 入口(IDOne 优先、飞书兜底,见
5
+ // pickLoginProvider),redirect 指向平台 landing 页并携带
6
+ // callback_url/state(经 OAuth state 往返透传);
7
+ // 2. 浏览器完成登录 → 平台回调页写 hermeshub_token → landing 页用平台
8
+ // 令牌换一次性 code → 跳转 callback_url(即本插件的 /agentone/callback
9
+ // 路由);
8
10
  // 3. exchangeCode:code 换新签发的平台访问令牌。
9
11
  import { randomBytes } from 'node:crypto';
10
12
 
@@ -14,10 +16,37 @@ export function newLoginState() {
14
16
  return randomBytes(16).toString('hex');
15
17
  }
16
18
 
17
- export function buildLoginUrl(platformUrl, callbackUrl, state) {
19
+ export function buildLoginUrl(platformUrl, callbackUrl, state, provider = 'feishu') {
18
20
  const landing = `/api/auth/plugin/landing?callback_url=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
19
21
  const redirect = encodeURIComponent(landing);
20
- return `${platformUrl.replace(/\/+$/, '')}/api/auth/feishu/login?redirect=${redirect}`;
22
+ const entry = provider === 'idone' ? '/api/auth/casdoor/login' : '/api/auth/feishu/login';
23
+ return `${platformUrl.replace(/\/+$/, '')}${entry}?redirect=${redirect}`;
24
+ }
25
+
26
+ /**
27
+ * 匿名拉取平台能力面(登录入口开关)。登录前无令牌,走公开的
28
+ * /api/auth/capabilities(门户登录页同源依赖)。任何失败(超时/非 2xx/
29
+ * 解析错)返回 null,调用方按能力未知处理。
30
+ */
31
+ export async function fetchAuthCapabilities(platformUrl) {
32
+ try {
33
+ const response = await platformFetch(`${platformUrl.replace(/\/+$/, '')}/api/auth/capabilities`, {}, 3000);
34
+ if (!response.ok) return null;
35
+ return await response.json();
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 按 capabilities 选登录入口:仅当 IDOne 已启用且邮箱锚点模式开启
43
+ * (平台统一登录语义,未绑定用户由后端按邮箱自动认人)才走 IDOne;
44
+ * 其余情况(未启用 / 非锚点 / 查询失败)一律回退飞书。
45
+ */
46
+ export function pickLoginProvider(caps) {
47
+ return caps?.casdoor_oauth_enabled === true && caps?.casdoor_email_anchor_enabled === true
48
+ ? 'idone'
49
+ : 'feishu';
21
50
  }
22
51
 
23
52
  export async function exchangeCode(platformUrl, code) {
package/lib/client.js CHANGED
@@ -176,6 +176,8 @@ window.__ModuleLoader__.load({
176
176
  const { complete } = props;
177
177
  const [phase, setPhase] = React.useState('checking'); // checking | idle | waiting
178
178
  const [error, setError] = React.useState('');
179
+ // 登录入口名(服务端按平台能力面路由:IDOne 优先、飞书兜底)
180
+ const [providerName, setProviderName] = React.useState('飞书');
179
181
 
180
182
  React.useEffect(() => {
181
183
  let cancelled = false;
@@ -183,8 +185,10 @@ window.__ModuleLoader__.load({
183
185
  try {
184
186
  const response = await fetch('/agentone/api/status');
185
187
  const status = await response.json();
186
- if (!cancelled && status?.logged_in) complete();
187
- else if (!cancelled) setPhase('idle');
188
+ if (cancelled) return;
189
+ if (status?.login_provider === 'idone') setProviderName('IDOne');
190
+ if (status?.logged_in) complete();
191
+ else setPhase('idle');
188
192
  } catch {
189
193
  if (!cancelled) setPhase('idle');
190
194
  }
@@ -245,8 +249,8 @@ window.__ModuleLoader__.load({
245
249
  'p',
246
250
  { style: bodyStyle },
247
251
  phase === 'waiting'
248
- ? '已打开授权窗口,完成飞书授权后这里会自动继续…'
249
- : '使用公司飞书账号登录,公司托管的模型与技能会自动配置,无需任何 API Key。',
252
+ ? `已打开授权窗口,完成${providerName}授权后这里会自动继续…`
253
+ : `使用公司${providerName}账号登录,公司托管的模型与技能会自动配置,无需任何 API Key。`,
250
254
  ),
251
255
  error
252
256
  ? React.createElement('p', { style: { ...bodyStyle, color: '#c0392b' } }, error)
@@ -261,7 +265,7 @@ window.__ModuleLoader__.load({
261
265
  onClick: startLogin,
262
266
  disabled: phase === 'waiting',
263
267
  },
264
- phase === 'waiting' ? '等待授权…' : '飞书登录',
268
+ phase === 'waiting' ? '等待授权…' : `${providerName}登录`,
265
269
  ),
266
270
  React.createElement(
267
271
  'button',
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // dsh-agentone 主入口:挂 /agentone/* 路由(dsh webServer 原生 Node handler)。
2
2
  //
3
- // M1(飞书登录)页面与 API:
3
+ // M1(登录,IDOne 优先、飞书兜底)页面与 API:
4
4
  // GET /agentone/ 状态页(登录/登出/模型同步状态)
5
- // GET /agentone/api/status {platform_url, logged_in, user_email, model}
6
- // POST /agentone/api/login {callback_url} → {login_url}(浏览器打开)
5
+ // GET /agentone/api/status {platform_url, logged_in, user_email, model, login_provider}
6
+ // POST /agentone/api/login {callback_url} → {login_url, provider}(浏览器打开)
7
7
  // GET /agentone/callback?code&state 平台 landing 跳回:换令牌后自动配置模型
8
8
  // POST /agentone/api/logout 清除本地凭据并移除平台模型配置
9
9
  // POST /agentone/api/model/sync 重新拉取平台模型并写入 settings.yaml
@@ -21,10 +21,12 @@ import {
21
21
  applyPlatformPlan,
22
22
  buildLoginUrl,
23
23
  exchangeCode,
24
+ fetchAuthCapabilities,
24
25
  fetchPlatformCapabilities,
25
26
  fetchPlatformPlan,
26
27
  fetchPlatformProfile,
27
28
  newLoginState,
29
+ pickLoginProvider,
28
30
  refreshPlatformToken,
29
31
  revokePlatformToken,
30
32
  } from './auth.js';
@@ -61,6 +63,29 @@ export const Config = z.object({
61
63
  /** 本次进程内的登录 CSRF state(/callback 比对后即失效)。 */
62
64
  const pendingStates = new Set();
63
65
 
66
+ // 登录入口判定缓存:key = platformUrl(避免切换平台后沿用旧判定)。
67
+ // 成功缓存 5 分钟(平台配置热重载后慢收敛可接受);查询失败只缓存 30 秒
68
+ // (避免平台瞬时抖动把入口钉死在飞书,也不让未登录状态轮询反复吃超时)。
69
+ const PROVIDER_CACHE_OK_MS = 5 * 60 * 1000;
70
+ const PROVIDER_CACHE_FAIL_MS = 30 * 1000;
71
+ const loginProviderCache = new Map();
72
+
73
+ /** 查平台能力面决定登录入口('idone' | 'feishu'),永抛错、永阻断登录。 */
74
+ async function resolveLoginProvider(platformUrl, log) {
75
+ const key = String(platformUrl || '').replace(/\/+$/, '');
76
+ const cached = loginProviderCache.get(key);
77
+ if (cached && Date.now() - cached.at < cached.ttl) return cached.provider;
78
+ const caps = await fetchAuthCapabilities(key);
79
+ const provider = pickLoginProvider(caps);
80
+ loginProviderCache.set(key, {
81
+ provider,
82
+ at: Date.now(),
83
+ ttl: caps ? PROVIDER_CACHE_OK_MS : PROVIDER_CACHE_FAIL_MS,
84
+ });
85
+ if (!caps) log?.warn?.(`[dsh-agentone] capabilities unavailable, login falls back to feishu: ${key}`);
86
+ return provider;
87
+ }
88
+
64
89
  function sendJson(response, status, data) {
65
90
  response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
66
91
  response.end(JSON.stringify(data));
@@ -451,7 +476,7 @@ const BUILTIN_PLUGIN_DESCRIPTIONS = {
451
476
  'dsh-im':
452
477
  'IM 桥核心:让 dsh 接入飞书等聊天工具——群内 @机器人 干活、收通知与审批卡片',
453
478
  [PLUGIN_PACKAGE_NAME]:
454
- 'AgentOne 平台集成:飞书登录、平台模型、SkillHub 技能与 CLI 授权(本插件)',
479
+ 'AgentOne 平台集成:IDOne 登录、平台模型、SkillHub 技能与 CLI 授权(本插件)',
455
480
  };
456
481
 
457
482
  /** 汇总 profile 插件状态:dependencies(去掉 in-box)+ 已装版本 + 激活态。 */
@@ -880,10 +905,13 @@ export function apply(ctx, config) {
880
905
  modelRetryAttempts = 0;
881
906
  }
882
907
  }
908
+ // 登录入口判定:UI 登录卡文案需要;托管实例不显示登录入口,跳过查询
909
+ const loginProvider = isInstanceManaged() ? 'feishu' : await resolveLoginProvider(config.platformUrl, log);
883
910
  sendJson(response, 200, {
884
911
  platform_url: config.platformUrl,
885
912
  logged_in: Boolean(userEmail),
886
913
  user_email: userEmail,
914
+ login_provider: loginProvider,
887
915
  // 托管实例:凭据由平台注入,页面据此显示「平台托管」并隐藏登录/登出
888
916
  managed: isInstanceManaged(),
889
917
  source: credentials?.source ?? null,
@@ -940,7 +968,12 @@ export function apply(ctx, config) {
940
968
  }
941
969
  const state = newLoginState();
942
970
  pendingStates.add(state);
943
- sendJson(response, 200, { login_url: buildLoginUrl(config.platformUrl, callbackUrl, state) });
971
+ // 入口按平台能力面路由:IDOne(锚点模式)优先,飞书兜底
972
+ const provider = await resolveLoginProvider(config.platformUrl, log);
973
+ sendJson(response, 200, {
974
+ login_url: buildLoginUrl(config.platformUrl, callbackUrl, state, provider),
975
+ provider,
976
+ });
944
977
  } catch (error) {
945
978
  log.warn(`[dsh-agentone] login failed: ${error?.message || error}`);
946
979
  sendJson(response, 500, { error: '发起登录失败' });
@@ -1030,7 +1063,7 @@ export function apply(ctx, config) {
1030
1063
  try {
1031
1064
  const credentials = await ensureFreshToken(config, log);
1032
1065
  if (!credentials) {
1033
- sendJson(response, 401, { error: '请先完成飞书登录' });
1066
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1034
1067
  return;
1035
1068
  }
1036
1069
  const result = await syncPlatformSkillhub(config, log, credentials);
@@ -1049,7 +1082,7 @@ export function apply(ctx, config) {
1049
1082
  try {
1050
1083
  const credentials = await ensureFreshToken(config, log);
1051
1084
  if (!credentials) {
1052
- sendJson(response, 401, { error: '请先完成飞书登录' });
1085
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1053
1086
  return;
1054
1087
  }
1055
1088
  const body = await readJsonBody(request);
@@ -1082,7 +1115,7 @@ export function apply(ctx, config) {
1082
1115
  try {
1083
1116
  const credentials = await ensureFreshToken(config, log);
1084
1117
  if (!credentials) {
1085
- sendJson(response, 401, { error: '请先完成飞书登录' });
1118
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1086
1119
  return;
1087
1120
  }
1088
1121
  const result = await syncPlanSkills(config, log, credentials);
@@ -1102,7 +1135,7 @@ export function apply(ctx, config) {
1102
1135
  try {
1103
1136
  const credentials = await ensureFreshToken(config, log);
1104
1137
  if (!credentials) {
1105
- sendJson(response, 401, { error: '请先完成飞书登录' });
1138
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1106
1139
  return;
1107
1140
  }
1108
1141
  const query = new URL(request.url, 'http://localhost').searchParams.get('q') || '';
@@ -1136,7 +1169,7 @@ export function apply(ctx, config) {
1136
1169
  try {
1137
1170
  const credentials = await loadCredentials(config);
1138
1171
  if (!credentials) {
1139
- sendJson(response, 401, { error: '请先完成飞书登录' });
1172
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1140
1173
  return;
1141
1174
  }
1142
1175
  const body = await readJsonBody(request);
@@ -1583,7 +1616,7 @@ export function apply(ctx, config) {
1583
1616
  try {
1584
1617
  const credentials = await loadCredentials(config);
1585
1618
  if (!credentials) {
1586
- sendJson(response, 401, { error: '请先完成飞书登录' });
1619
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1587
1620
  return;
1588
1621
  }
1589
1622
  const plan = await fetchPlatformPlan(config.platformUrl, credentials.access_token);
@@ -1602,7 +1635,7 @@ export function apply(ctx, config) {
1602
1635
  try {
1603
1636
  const credentials = await loadCredentials(config);
1604
1637
  if (!credentials) {
1605
- sendJson(response, 401, { error: '请先完成飞书登录' });
1638
+ sendJson(response, 401, { error: '请先完成 AgentOne 登录' });
1606
1639
  return;
1607
1640
  }
1608
1641
  const body = await readJsonBody(request);
package/lib/page.js CHANGED
@@ -1115,7 +1115,7 @@ const renderAbout = (s) => {
1115
1115
  const lines = [
1116
1116
  aboutVersionRow(),
1117
1117
  '<div class="kv"><span class="k">登录账号</span><span class="v">' + esc(s.user_email || '未登录') + '</span></div>',
1118
- '<div class="kv"><span class="k">授权方式</span><span class="v muted">' + (s.managed ? '平台托管(实例内自动授权,无需登录)' : '飞书登录') + '</span></div>',
1118
+ '<div class="kv"><span class="k">授权方式</span><span class="v muted">' + (s.managed ? '平台托管(实例内自动授权,无需登录)' : (s.login_provider === 'idone' ? 'IDOne 登录' : '飞书登录')) + '</span></div>',
1119
1119
  '<div class="kv"><span class="k">所属套餐</span><span class="v">' + esc(s.capabilities?.plan_name || '—') + '</span></div>',
1120
1120
  '<div class="kv"><span class="k">平台地址</span><span class="v muted">' + esc(s.platform_url || '—') + '</span></div>',
1121
1121
  '<div class="kv"><span class="k">开发者</span><span class="v"><a href="mailto:chumingjun@cc-pg.cn">chumingjun@cc-pg.cn</a></span></div>',
@@ -1173,6 +1173,16 @@ const syncModelNow = (btn) => withLoading(
1173
1173
  },
1174
1174
  null,
1175
1175
  );
1176
+ // 登录入口文案:平台统一登录(IDOne)时覆盖静态默认的飞书文案。静态默认
1177
+ // 「飞书登录」保留——capabilities 失败回退飞书的场景它就是正确文案(smoke
1178
+ // 也断言状态页 HTML 含该默认值)。已点「发起登录」的按钮不回写(dataset.opened)。
1179
+ const applyLoginProvider = (s) => {
1180
+ if (s?.login_provider !== 'idone') return;
1181
+ const btn = $('login-btn');
1182
+ if (btn && !btn.dataset.opened) btn.textContent = 'IDOne 登录';
1183
+ const sub = loginCard.querySelector('p:not(.big)');
1184
+ if (sub) sub.textContent = 'IDOne 账号一键授权,无需任何 API Key;套餐审批通过后模型自动配置。';
1185
+ };
1176
1186
  const refresh = async () => {
1177
1187
  let s;
1178
1188
  try {
@@ -1206,6 +1216,7 @@ const refresh = async () => {
1206
1216
  bootCard.hidden = true;
1207
1217
  loginCard.hidden = s.logged_in;
1208
1218
  mainCards.hidden = !s.logged_in;
1219
+ applyLoginProvider(s);
1209
1220
  // 托管态不给登出按钮:授权来自容器 env,登出既清不掉根因、重启又会自动登录
1210
1221
  logoutBtn.hidden = !s.logged_in || Boolean(s.managed);
1211
1222
  if (s.logged_in && s.model) {
@@ -1286,7 +1297,9 @@ $('login-btn').onclick = async () => {
1286
1297
  if (!resp.ok) { notify(data.error || '发起登录失败', 'err'); return; }
1287
1298
  // 新标签页(顶层上下文)打开:iframe 内跨源 localStorage 被浏览器分区隔离
1288
1299
  window.open(data.login_url, '_blank');
1289
- // 按钮保持可点:授权窗被用户关掉后可立即重新发起,无需等 2 分钟
1300
+ // 按钮保持可点:授权窗被用户关掉后可立即重新发起,无需等 2 分钟;
1301
+ // 标记已发起,避免 5s 轮询的 applyLoginProvider 把提示文案改回入口名
1302
+ btn.dataset.opened = '1';
1290
1303
  btn.textContent = '已打开授权窗口,完成后自动继续(可重开)';
1291
1304
  };
1292
1305
  $('sync-btn').onclick = () => syncModelNow($('sync-btn'));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-agentone",
3
- "version": "0.5.37",
4
- "description": "AgentOne 平台集成:平台模型、SkillHub 技能、套餐与 CLI;lark-cli 首装一键初始化飞书应用;平台托管实例免登录自动授权",
3
+ "version": "0.5.38",
4
+ "description": "AgentOne 平台集成:IDOne 登录(飞书兜底)、平台模型、SkillHub 技能、套餐与 CLI;lark-cli 首装一键初始化飞书应用;平台托管实例免登录自动授权",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "smoke": "node scripts/env-matrix-smoke.mjs"