weaver-work-cli 0.1.25 → 0.1.28

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.
Files changed (173) hide show
  1. package/README.md +35 -30
  2. package/dist/cmd/root.js +1 -0
  3. package/dist/cmd/setup/index.js +1 -1
  4. package/dist/core/runtime-agent.js +19 -0
  5. package/dist/internal/e10/auth/commands.js +48 -33
  6. package/dist/internal/e10/auth/messages.js +3 -0
  7. package/dist/internal/e10/auth/session.js +8 -0
  8. package/dist/internal/e10/auth/xiaoe.js +62 -43
  9. package/dist/internal/e10/auth-recovery.js +148 -0
  10. package/dist/internal/e10/client.js +47 -15
  11. package/dist/internal/e10/context.js +9 -31
  12. package/dist/internal/e10/index.js +1 -0
  13. package/dist/internal/e10/request-runtime.js +51 -9
  14. package/dist/shortcuts/archive/host.js +3 -2
  15. package/dist/shortcuts/archive/manifest.js +1 -1
  16. package/dist/shortcuts/archive-oa/constants.js +66 -0
  17. package/dist/shortcuts/archive-oa/continuation.js +70 -0
  18. package/dist/shortcuts/archive-oa/errors.js +53 -0
  19. package/dist/shortcuts/archive-oa/host.js +158 -0
  20. package/dist/shortcuts/archive-oa/index.js +225 -0
  21. package/dist/shortcuts/archive-oa/manifest.js +62 -0
  22. package/dist/shortcuts/archive-oa/operations/borrow-car.js +159 -0
  23. package/dist/shortcuts/archive-oa/operations/borrow-url.js +108 -0
  24. package/dist/shortcuts/archive-oa/operations/confirm.js +48 -0
  25. package/dist/shortcuts/archive-oa/operations/contract.js +120 -0
  26. package/dist/shortcuts/archive-oa/operations/locate.js +50 -0
  27. package/dist/shortcuts/archive-oa/operations/registry.js +40 -0
  28. package/dist/shortcuts/archive-oa/operations/shared.js +181 -0
  29. package/dist/shortcuts/archive-oa/operations/types.js +45 -0
  30. package/dist/shortcuts/archive-oa/operations.js +13 -0
  31. package/dist/shortcuts/asset/host.js +7 -6
  32. package/dist/shortcuts/blog/host.js +4 -3
  33. package/dist/shortcuts/caizhilian/host.js +3 -2
  34. package/dist/shortcuts/caizhilian/manifest.js +1 -1
  35. package/dist/shortcuts/calendar/host.js +2 -1
  36. package/dist/shortcuts/contract/host.js +34 -59
  37. package/dist/shortcuts/ehr/host.js +2 -1
  38. package/dist/shortcuts/esb/host.js +2 -1
  39. package/dist/shortcuts/fna/host.js +6 -3
  40. package/dist/shortcuts/fna/manifest.js +10 -5
  41. package/dist/shortcuts/fna/operations/expense-chart.js +162 -0
  42. package/dist/shortcuts/fna/operations/registry.js +2 -0
  43. package/dist/shortcuts/hrm/host.js +2 -1
  44. package/dist/shortcuts/im/host.js +3 -2
  45. package/dist/shortcuts/index.js +2 -0
  46. package/dist/shortcuts/invoice/host.js +9 -7
  47. package/dist/shortcuts/invoice/manifest.js +38 -47
  48. package/dist/shortcuts/invoice/operations/issuing-qys.js +328 -0
  49. package/dist/shortcuts/invoice/operations/registry.js +2 -3
  50. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  51. package/dist/shortcuts/mail/host.js +2 -1
  52. package/dist/shortcuts/meeting/host.js +2 -1
  53. package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -12
  54. package/dist/shortcuts/okr/host.js +2 -1
  55. package/dist/shortcuts/plan/host.js +2 -1
  56. package/dist/shortcuts/project/host.js +2 -1
  57. package/dist/shortcuts/workflow/endpoints.js +11 -0
  58. package/dist/shortcuts/workflow/host.js +5 -7
  59. package/dist/shortcuts/workflow/index.js +6 -36
  60. package/dist/shortcuts/workflow/operations/chatscene.js +252 -0
  61. package/dist/shortcuts/workflow/operations/form.js +1047 -191
  62. package/dist/shortcuts/workflow/operations/read.js +137 -8
  63. package/dist/shortcuts/workflow/operations/sign-input.js +106 -0
  64. package/dist/shortcuts/workflow/operations/write.js +9 -1
  65. package/dist/shortcuts/workflow/stdout-compact.js +16 -1
  66. package/docs/SKILL.md +1 -1
  67. package/docs/_catalog.md +1 -1
  68. package/docs/agent-invoice.md +1 -1
  69. package/docs/archive-oa.md +79 -0
  70. package/docs/caizhilian.md +1 -1
  71. package/docs/e10-auth.md +7 -6
  72. package/docs/fna.md +7 -3
  73. package/docs/invoice.md +4 -6
  74. package/package.json +1 -1
  75. package/skill-template/business-info.json +20 -1
  76. package/skill-template/domains/archive-oa.md +19 -0
  77. package/skill-template/domains/caizhilian.md +2 -1
  78. package/skill-template/domains/qiyecheng.md +1 -0
  79. package/skill-template/domains/yepiaotong.md +8 -4
  80. package/skill-template/skill-template.md +1 -0
  81. package/skills/weaver-e10-archive-oa/SKILL.md +159 -0
  82. package/skills/weaver-e10-archive-oa/references/archive-locate.md +49 -0
  83. package/skills/weaver-e10-archive-oa/references/borrow-car.md +76 -0
  84. package/skills/weaver-e10-archive-oa/references/borrow-flow.md +53 -0
  85. package/skills/weaver-e10-archive-oa/references/contract-search.md +63 -0
  86. package/skills/weaver-e10-archive-oa/references/disabled-capabilities.md +29 -0
  87. package/skills/weaver-e10-archive-oa/references/qys-contract-search.md +47 -0
  88. package/skills/weaver-e10-archive-oa/references/source-manifest.json +400 -0
  89. package/skills/weaver-e10-blog/SKILL.md +6 -1
  90. package/skills/weaver-e10-blog/references/blog-query.md +3 -1
  91. package/skills/weaver-e10-builder/SKILL.md +22 -0
  92. package/skills/weaver-e10-caizhilian/SKILL.md +47 -16
  93. package/skills/weaver-e10-caizhilian/references/action-flow.md +70 -0
  94. package/skills/weaver-e10-caizhilian/references/agent-entry.md +12 -4
  95. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +207 -0
  96. package/skills/weaver-e10-caizhilian/references/collaboration.md +5 -3
  97. package/skills/weaver-e10-caizhilian/references/document.md +33 -11
  98. package/skills/weaver-e10-caizhilian/references/file-collection.md +57 -54
  99. package/skills/weaver-e10-caizhilian/references/file-upload.md +5 -6
  100. package/skills/weaver-e10-caizhilian/references/files.md +9 -3
  101. package/skills/weaver-e10-caizhilian/references/folder.md +48 -16
  102. package/skills/weaver-e10-caizhilian/references/host-executor-prompt.md +32 -0
  103. package/skills/weaver-e10-caizhilian/references/host-platform-integration.md +46 -0
  104. package/skills/weaver-e10-caizhilian/references/rule-index.md +56 -0
  105. package/skills/weaver-e10-caizhilian/references/setting-attach.md +17 -54
  106. package/skills/weaver-e10-caizhilian/references/setting-backup.md +16 -63
  107. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +24 -64
  108. package/skills/weaver-e10-caizhilian/references/setting-storage-docs-summary.md +126 -0
  109. package/skills/weaver-e10-caizhilian/references/setting-storage-field-map.md +57 -0
  110. package/skills/weaver-e10-caizhilian/references/setting-storage.md +143 -548
  111. package/skills/weaver-e10-caizhilian/references/source-manifest.json +7 -7
  112. package/skills/weaver-e10-caizhilian/references/task-workflow.md +87 -0
  113. package/skills/weaver-e10-caizhilian/references/withheld.md +21 -45
  114. package/skills/weaver-e10-caizhilian/references/write-protocol.md +7 -0
  115. package/skills/weaver-e10-calendar/SKILL.md +6 -1
  116. package/skills/weaver-e10-esb/SKILL.md +8 -0
  117. package/skills/weaver-e10-esb/references/input-format.md +3 -1
  118. package/skills/weaver-e10-esb/references/source-manifest.json +5 -5
  119. package/skills/weaver-e10-hrm/SKILL.md +6 -1
  120. package/skills/weaver-e10-hrm/references/employee.md +1 -1
  121. package/skills/weaver-e10-hrm/references/org.md +1 -1
  122. package/skills/weaver-e10-hrm/references/position.md +1 -1
  123. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  124. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +1 -1
  125. package/skills/weaver-e10-jiuchuanhui/SKILL.md +6 -1
  126. package/skills/weaver-e10-jucailin/SKILL.md +6 -1
  127. package/skills/weaver-e10-jucailin/references/hr-contact.md +1 -1
  128. package/skills/weaver-e10-jucailin/references/salary.md +1 -1
  129. package/skills/weaver-e10-mail/SKILL.md +6 -1
  130. package/skills/weaver-e10-meeting/SKILL.md +11 -6
  131. package/skills/weaver-e10-meeting/references/meeting-eb.md +7 -3
  132. package/skills/weaver-e10-meeting/references/meeting-queries.md +3 -1
  133. package/skills/weaver-e10-meeting/references/meeting-verification.md +4 -2
  134. package/skills/weaver-e10-okr/SKILL.md +6 -1
  135. package/skills/weaver-e10-okr/references/source-manifest.json +5 -5
  136. package/skills/weaver-e10-plan/SKILL.md +6 -1
  137. package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +3 -1
  138. package/skills/weaver-e10-plan/references/plan-report-query.md +3 -1
  139. package/skills/weaver-e10-plan/references/source-manifest.json +7 -7
  140. package/skills/weaver-e10-qiyecheng/SKILL.md +16 -6
  141. package/skills/weaver-e10-qiyecheng/references/catalog.md +6 -3
  142. package/skills/weaver-e10-qiyecheng/references/expense-center.md +2 -0
  143. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +78 -0
  144. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +87 -33
  145. package/skills/weaver-e10-shared/SKILL.md +53 -28
  146. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +22 -20
  147. package/skills/weaver-e10-shared/references/non-interactive-environment.md +13 -13
  148. package/skills/weaver-e10-shared/references/weaver-e10-installation.md +3 -1
  149. package/skills/weaver-e10-wenshuding/SKILL.md +6 -1
  150. package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
  151. package/skills/weaver-e10-workflow/references/source-manifest.json +33 -24
  152. package/skills/weaver-e10-workflow/references/workflow-create.md +41 -26
  153. package/skills/weaver-e10-yepiaotong/SKILL.md +23 -18
  154. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +87 -45
  155. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +152 -0
  156. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +210 -0
  157. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +59 -36
  158. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +63 -512
  159. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +205 -346
  160. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -5
  161. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +231 -344
  162. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +5 -5
  163. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +201 -162
  164. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +121 -145
  165. package/skills/weaver-e10-yimiaoban/SKILL.md +9 -2
  166. package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +1 -1
  167. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -1
  168. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +9 -9
  169. package/skills/weaver-e10-ziguanjia/SKILL.md +6 -1
  170. package/dist/shortcuts/invoice/operations/issuing.js +0 -198
  171. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +0 -161
  172. package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +0 -107
  173. package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +0 -119
@@ -1,9 +1,14 @@
1
+ import { isE10AuthenticationRequired, sameE10SessionIdentity, } from './auth-recovery.js';
2
+ class E10AuthenticationContextChangedError extends Error {
3
+ }
1
4
  export class E10ApiClient {
2
5
  auth;
3
6
  retries;
4
- constructor(auth, retries = 3) {
7
+ recoverAuthentication;
8
+ constructor(auth, retries = 3, recoverAuthentication) {
5
9
  this.auth = auth;
6
10
  this.retries = retries;
11
+ this.recoverAuthentication = recoverAuthentication;
7
12
  }
8
13
  getBaseUrl() {
9
14
  return this.auth.getBaseUrl();
@@ -56,31 +61,51 @@ export class E10ApiClient {
56
61
  }
57
62
  async requestRaw(url, init) {
58
63
  let lastError = null;
64
+ let authenticationRecoveryAttempted = false;
59
65
  for (let attempt = 1; attempt <= this.retries; attempt++) {
60
66
  try {
61
67
  const resp = await fetch(url, { ...init, redirect: 'manual' });
68
+ const text = await resp.text();
69
+ let value = null;
70
+ if (text) {
71
+ try {
72
+ value = JSON.parse(text);
73
+ }
74
+ catch {
75
+ value = text;
76
+ }
77
+ }
78
+ if (!authenticationRecoveryAttempted && isE10AuthenticationRequired({
79
+ status: resp.status,
80
+ headers: resp.headers,
81
+ text,
82
+ value,
83
+ })) {
84
+ authenticationRecoveryAttempted = true;
85
+ const recovered = await this.recoverAuthentication?.(this.auth) || null;
86
+ if (recovered) {
87
+ if (!sameE10SessionIdentity(this.auth, recovered)) {
88
+ throw new E10AuthenticationContextChangedError('XiaoE automatic authentication recovery changed the E10 login context');
89
+ }
90
+ this.auth.replaceWith(recovered);
91
+ refreshAuthenticationHeaders(init, this.auth);
92
+ attempt -= 1; // the one authentication retry does not consume the transport retry budget
93
+ continue;
94
+ }
95
+ }
96
+ // Preserve the legacy password-based refresh for non-XiaoE callers.
62
97
  if (resp.status === 302) {
63
98
  const ok = await this.auth.ensureValid();
64
99
  if (!ok)
65
100
  throw new Error('E10 session expired; please login again');
66
- const headers = (init.headers || {});
67
- headers.eteamsid = this.auth.eteamsId;
68
- headers.cookie = this.auth.cookieHeader();
69
- headers['user-agent'] = this.auth.uaHeader();
70
- init.headers = headers;
101
+ refreshAuthenticationHeaders(init, this.auth);
71
102
  continue;
72
103
  }
73
- const text = await resp.text();
74
- if (!text)
75
- return null;
76
- try {
77
- return JSON.parse(text);
78
- }
79
- catch {
80
- return text;
81
- }
104
+ return value;
82
105
  }
83
106
  catch (error) {
107
+ if (error instanceof E10AuthenticationContextChangedError)
108
+ throw error;
84
109
  lastError = error instanceof Error ? error : new Error(String(error));
85
110
  if (attempt < this.retries) {
86
111
  const delay = Math.min(1000 * Math.pow(2, attempt - 1), 8000);
@@ -102,3 +127,10 @@ export class E10ApiClient {
102
127
  return { body, boundary };
103
128
  }
104
129
  }
130
+ function refreshAuthenticationHeaders(init, session) {
131
+ const headers = (init.headers || {});
132
+ headers.eteamsid = session.eteamsId;
133
+ headers.cookie = session.cookieHeader();
134
+ headers['user-agent'] = session.uaHeader();
135
+ init.headers = headers;
136
+ }
@@ -1,48 +1,26 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
- import { E10AuthSession, getActiveProfile, readProfileConfig } from './auth/session.js';
3
- import { createXiaoEUnifiedLoginUnavailableError, hasXiaoEEnv, resolveXiaoEPreferredBaseUrl, shouldUseXiaoEBrowserFallback, tryXiaoEUnifiedLogin, } from './auth/xiaoe.js';
2
+ import { E10_AUTH_REQUIRED_MESSAGE } from './auth/messages.js';
3
+ import { E10AuthSession } from './auth/session.js';
4
4
  import { E10ApiClient } from './client.js';
5
- function shouldPreferXiaoEUnifiedLogin(options, env) {
6
- return !options.auth && !options.profile && hasXiaoEEnv(env);
7
- }
5
+ import { recoverXiaoEAuthSession } from './auth-recovery.js';
8
6
  export async function getE10Session(ctx, load = {}) {
9
7
  const options = ctx.getGlobalOptions();
10
- if (shouldPreferXiaoEUnifiedLogin(options, ctx.env)) {
11
- const profileBaseUrl = readProfileConfig(getActiveProfile()).baseUrl;
12
- const xiaoELogin = await tryXiaoEUnifiedLogin({
13
- env: ctx.env,
14
- baseUrl: options.baseUrl,
15
- profileBaseUrl,
16
- allowBundledEnvScript: true,
17
- allowTokenScript: true,
18
- logger: ctx.logger,
19
- });
20
- if (xiaoELogin)
21
- return xiaoELogin.session;
22
- return null;
23
- }
24
8
  return E10AuthSession.fromOptions(options, ctx.cwd, load);
25
9
  }
26
10
  export async function requireE10Session(ctx, load = {}) {
27
11
  const session = await getE10Session(ctx, load);
28
12
  if (!session) {
29
- const options = ctx.getGlobalOptions();
30
- if (hasXiaoEEnv(ctx.env) && !shouldUseXiaoEBrowserFallback()) {
31
- const baseUrl = resolveXiaoEPreferredBaseUrl({
32
- explicitBaseUrl: options.baseUrl,
33
- profileBaseUrl: readProfileConfig(getActiveProfile()).baseUrl,
34
- env: ctx.env,
35
- });
36
- throw createXiaoEUnifiedLoginUnavailableError(baseUrl);
37
- }
38
- throw new CliError('E10 auth not found. In XiaoE, run weaver-work-cli auth xiaoe first; if unified login is unavailable, ask the user for the E10 login domain (base URL), then run: weaver-work-cli auth login --base-url <url> --agent_type <agent_type>.', ExitCode.AuthError);
13
+ throw new CliError(E10_AUTH_REQUIRED_MESSAGE, ExitCode.AuthError);
39
14
  }
40
15
  return session;
41
16
  }
42
17
  export async function getE10Client(ctx) {
43
18
  const session = await getE10Session(ctx);
44
- return session ? new E10ApiClient(session) : null;
19
+ return session
20
+ ? new E10ApiClient(session, 3, (current) => recoverXiaoEAuthSession(ctx, current))
21
+ : null;
45
22
  }
46
23
  export async function requireE10Client(ctx) {
47
- return new E10ApiClient(await requireE10Session(ctx));
24
+ const session = await requireE10Session(ctx);
25
+ return new E10ApiClient(session, 3, (current) => recoverXiaoEAuthSession(ctx, current));
48
26
  }
@@ -1,3 +1,4 @@
1
1
  export { E10ApiClient } from './client.js';
2
2
  export { getE10Client, getE10Session, requireE10Client, requireE10Session } from './context.js';
3
3
  export { E10ApiError, E10RequestRuntime, createE10RequestRuntime } from './request-runtime.js';
4
+ export { canAutomaticallyRecoverXiaoE, isE10AuthenticationRequired, recoverXiaoEAuthSession, } from './auth-recovery.js';
@@ -1,4 +1,5 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
+ import { isE10AuthenticationRequired, recoverXiaoEAuthSession, sameE10SessionIdentity, } from './auth-recovery.js';
2
3
  const DEFAULT_TIMEOUT_MS = 30_000;
3
4
  const DEFAULT_BIG_INT_STRINGS = true;
4
5
  const RESERVED_HEADERS = [
@@ -29,11 +30,14 @@ export class E10RequestRuntime {
29
30
  interceptors;
30
31
  fetchFn;
31
32
  attachSessionTicket;
33
+ recoverAuthentication;
32
34
  constructor(ctx, session, dependencies = {}) {
33
35
  this.ctx = ctx;
34
36
  this.session = session;
35
37
  this.fetchFn = dependencies.fetch || globalThis.fetch;
36
38
  this.attachSessionTicket = dependencies.attachSessionTicket === true;
39
+ this.recoverAuthentication = dependencies.recoverAuthentication
40
+ || ((current) => recoverXiaoEAuthSession(this.ctx, current));
37
41
  this.interceptors = [
38
42
  ...(this.ctx.hooks?.getE10RequestInterceptors?.() || []),
39
43
  ...(dependencies.interceptors || []),
@@ -47,9 +51,8 @@ export class E10RequestRuntime {
47
51
  }
48
52
  async request(request) {
49
53
  const mutable = this.prepareRequest(request);
50
- const sessionContext = this.sessionContext();
51
54
  try {
52
- await this.runBeforeRequest(mutable, sessionContext);
55
+ await this.runBeforeRequest(mutable, this.sessionContext());
53
56
  this.assertPath(mutable.path);
54
57
  if (mutable.path !== request.path) {
55
58
  throw new E10ApiError('policy', 'path_rewrite_denied', 'E10 API 拦截器不能修改请求路径', {
@@ -58,19 +61,58 @@ export class E10RequestRuntime {
58
61
  });
59
62
  }
60
63
  stripReservedHeaders(mutable.headers);
61
- const url = withQuery(this.session.getBaseUrl(), mutable.path, mutable.query);
62
- const headers = this.composeHeaders(mutable, request.trustedHeaders);
63
- const init = this.buildFetchInit(mutable, headers);
64
- const response = await this.fetchWithTimeout(url, init, mutable.timeoutMs);
65
- const parsed = await this.readResponse(response, mutable.responseType, mutable.bigIntStrings, mutable.maxResponseBytes, mutable.timeoutMs);
66
- await this.runAfterResponse(mutable, sessionContext, parsed);
64
+ const requestEteamsId = this.session.eteamsId;
65
+ let parsed = await this.executeRequest(mutable, request.trustedHeaders);
66
+ if (!mutable.streamBody
67
+ && isE10AuthenticationRequired(parsed)
68
+ && await this.recoverExpiredSession(requestEteamsId)) {
69
+ // Authentication rejection is a definitive pre-business failure. Retry
70
+ // exactly once with the new session; never retry transport/partial errors.
71
+ parsed = await this.executeRequest(mutable, request.trustedHeaders);
72
+ }
73
+ await this.runAfterResponse(mutable, this.sessionContext(), parsed);
67
74
  return parsed;
68
75
  }
69
76
  catch (error) {
70
- await this.runOnError(mutable, sessionContext, error);
77
+ await this.runOnError(mutable, this.sessionContext(), error);
71
78
  throw error;
72
79
  }
73
80
  }
81
+ async executeRequest(request, trustedHeaders = {}) {
82
+ const url = withQuery(this.session.getBaseUrl(), request.path, request.query);
83
+ const headers = this.composeHeaders(request, trustedHeaders);
84
+ const init = this.buildFetchInit(request, headers);
85
+ const response = await this.fetchWithTimeout(url, init, request.timeoutMs);
86
+ return this.readResponse(response, request.responseType, request.bigIntStrings, request.maxResponseBytes, request.timeoutMs);
87
+ }
88
+ async recoverExpiredSession(requestEteamsId) {
89
+ // Another concurrent request may already have refreshed the shared session
90
+ // object while this response was in flight.
91
+ if (this.session.eteamsId && this.session.eteamsId !== requestEteamsId)
92
+ return true;
93
+ let recovered;
94
+ try {
95
+ recovered = await this.recoverAuthentication(this.session);
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ if (!recovered)
101
+ return false;
102
+ if (!sameE10SessionIdentity(this.session, recovered)) {
103
+ throw new E10ApiError('authentication', 'session_context_changed', 'XiaoE 自动恢复后的登录身份或 E10 环境发生变化,本次请求未重试', { exitCode: ExitCode.AuthError, retryable: false });
104
+ }
105
+ const original = this.session;
106
+ if (typeof original.replaceWith === 'function') {
107
+ original.replaceWith(recovered);
108
+ this.session = original;
109
+ }
110
+ else {
111
+ // Structural test doubles and older embedders may not expose replaceWith.
112
+ this.session = recovered;
113
+ }
114
+ return true;
115
+ }
74
116
  prepareRequest(request) {
75
117
  return {
76
118
  module: request.module,
@@ -1,3 +1,4 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -151,7 +152,7 @@ export function createArchiveHost(ctx, dependencies = {}) {
151
152
  async function request(request) {
152
153
  const response = await doRequest(request, false).catch((error) => mapRuntimeNetworkError(error));
153
154
  if (response.status === 401 || response.status === 302) {
154
- throw archiveAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
155
+ throw archiveAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
155
156
  }
156
157
  if (response.parseError) {
157
158
  if (response.status < 200 || response.status >= 300) {
@@ -182,7 +183,7 @@ export function createArchiveHost(ctx, dependencies = {}) {
182
183
  async function requestBinary(request) {
183
184
  const response = await doRequest(request, true).catch((error) => mapRuntimeNetworkError(error));
184
185
  if (response.status === 401 || response.status === 302) {
185
- throw archiveAuthError('session_expired', 'E10 登录态已失效,请先登录', { httpStatus: response.status });
186
+ throw archiveAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
186
187
  }
187
188
  if (response.status < 200 || response.status >= 300) {
188
189
  throw new ArchiveCliError('api', 'http_error', `档案下载 HTTP ${response.status}`, {
@@ -20,7 +20,7 @@ export const archiveManifest = {
20
20
  capabilities: ['fonds', 'prelib', 'search', 'borrowCar', 'borrowList', 'flow', 'download', 'upload'],
21
21
  auth: {
22
22
  required: 'e10-session',
23
- note: '所有操作都要求已登录 E10(weaver-work-cli auth login --base-url <域名>),未登录时返回 auth_error。',
23
+ note: '所有操作都要求已登录 E10;未登录时返回 auth_error,调用方应根据当前宿主环境选择登录方式。',
24
24
  },
25
25
  rules: [
26
26
  '写操作(*.apply)必须先执行同名的 prepare 拿到 continuation,再带 confirm=true 执行 apply;CLI 用 --yes 表示确认。',
@@ -0,0 +1,66 @@
1
+ /**
2
+ * archive-oa 固定业务参数。
3
+ *
4
+ * 泛微商务合同与契约锁协作采纳是两套独立的应用/列表,参数值不同,禁止交叉混用。
5
+ * 来源:`source/weaver-e10-archive-oa.zip` 内 10 篇接口文档(catalog.md 登记)。
6
+ */
7
+ /** 加入借阅车 / 发起借阅的单次条数上限(接口侧限制)。 */
8
+ export const MAX_BORROW_ITEMS = 6;
9
+ /** 发起借阅流程创建页的流程 ID(前端 `flowpage/fullCreate/<flowId>`)。 */
10
+ export const BORROW_FLOW_ID = '100003460000002090';
11
+ /** 借阅车记录 `fb` 即界面「分部」列:0 = 泛微、1 = 契约锁。 */
12
+ export const FLOW_FB_LABELS = {
13
+ '0': '泛微',
14
+ '1': '契约锁',
15
+ };
16
+ /** 档案系统:按合同号定位档案的自定义接口标识。 */
17
+ export const ARCHIVE_LOCATE_INTERFACE_PK = 'getArcIdByContractNo';
18
+ export const ARCHIVE_LOCATE_PATH = `/api/ebuilder/form/formdata/v2/getFormDataList/${ARCHIVE_LOCATE_INTERFACE_PK}`;
19
+ /** 泛微商务合同(案例台账)。 */
20
+ export const WEAVER_CONTRACT = {
21
+ source: 'weaver',
22
+ passId: '850741972510941185',
23
+ listId: '850741985437786120',
24
+ filterListId: '850741985437786120',
25
+ cusMenuId: '5408862605522555751',
26
+ urlPageTitle: '5rOb5b6u5qGI5L6L',
27
+ };
28
+ export const WEAVER_BORROW_CAR = {
29
+ source: 'weaver',
30
+ passId: '882393476189437952',
31
+ listId: '882393480417296390',
32
+ cusMenuId: '5408862605522555751',
33
+ urlPageTitle: '5rOb5b6u5qGI5L6L',
34
+ };
35
+ export const WEAVER_ESB_ADD = {
36
+ source: 'weaver',
37
+ esbActionIds: '882395911377174530',
38
+ objId: '850741972510941185',
39
+ listId: '850741985437786120',
40
+ successText: '【加入借阅车】执行成功!',
41
+ };
42
+ /** 契约锁协作采纳(用户常称「案例」)。 */
43
+ export const QYS_CONTRACT = {
44
+ source: 'qys',
45
+ passId: '850742853046345728',
46
+ listId: '1190611964371984450',
47
+ cusMenuId: '5408862605522555752',
48
+ urlPageTitle: '5aWR57qm6ZSB5qGI5L6L',
49
+ };
50
+ export const QYS_BORROW_CAR = {
51
+ source: 'qys',
52
+ passId: '882393476189437952',
53
+ listId: '883819516961857839',
54
+ cusMenuId: '5408862605522555752',
55
+ urlPageTitle: '5aWR57qm6ZSB5qGI5L6L',
56
+ };
57
+ export const QYS_ESB_ADD = {
58
+ source: 'qys',
59
+ esbActionIds: '892706856573648911',
60
+ objId: '850742853046345728',
61
+ listId: '1190611964371984450',
62
+ successText: '【契约锁加入借阅车】执行成功!',
63
+ };
64
+ /** 契约锁默认过滤「处理进度 = 已归档」。 */
65
+ export const QYS_DEFAULT_ARCHIVED_FIELD = 'cljd';
66
+ export const QYS_DEFAULT_ARCHIVED_VALUE = '已归档';
@@ -0,0 +1,70 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+ import { archiveOaValidationError } from './errors.js';
7
+ function stateRoot(ctx) {
8
+ const configured = ctx?.config?.path();
9
+ if (configured)
10
+ return resolve(configured, '..');
11
+ return process.env.WEAVER_WORK_CLI_HOME || process.env.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
12
+ }
13
+ async function loadKey(ctx) {
14
+ const keysDir = resolve(stateRoot(ctx), 'keys');
15
+ const filename = resolve(keysDir, 'archive-oa-continuation.key');
16
+ await mkdir(keysDir, { recursive: true, mode: 0o700 });
17
+ if (existsSync(filename))
18
+ return readFile(filename);
19
+ const key = randomBytes(32);
20
+ try {
21
+ await writeFile(filename, key, { flag: 'wx', mode: 0o600 });
22
+ if (process.platform !== 'win32')
23
+ await chmod(filename, 0o600).catch(() => undefined);
24
+ return key;
25
+ }
26
+ catch {
27
+ return readFile(filename);
28
+ }
29
+ }
30
+ function encode(value) {
31
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
32
+ }
33
+ function signature(payload, key) {
34
+ return createHmac('sha256', key).update(payload).digest('base64url');
35
+ }
36
+ export function createArchiveOaContinuationSigner(ctx) {
37
+ return {
38
+ async issue(payload, ttlMs = 10 * 60_000) {
39
+ const issuedAt = Date.now();
40
+ const body = encode({ ...payload, issuedAt, expiresAt: issuedAt + ttlMs });
41
+ const sig = signature(body, await loadKey(ctx));
42
+ return `${body}.${sig}`;
43
+ },
44
+ async verify(token, expectedOperation) {
45
+ if (typeof token !== 'string' || !token.includes('.')) {
46
+ throw archiveOaValidationError('continuation_invalid', 'continuation 无效');
47
+ }
48
+ const [body, provided] = token.split('.');
49
+ const expected = signature(body, await loadKey(ctx));
50
+ const valid = provided.length === expected.length
51
+ && timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
52
+ if (!valid)
53
+ throw archiveOaValidationError('continuation_invalid', 'continuation 签名校验失败');
54
+ let value;
55
+ try {
56
+ value = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
57
+ }
58
+ catch {
59
+ throw archiveOaValidationError('continuation_invalid', 'continuation 内容无法解析');
60
+ }
61
+ if (value.operation !== expectedOperation) {
62
+ throw archiveOaValidationError('continuation_mismatch', 'continuation 不属于当前操作');
63
+ }
64
+ if (!Number.isSafeInteger(value.expiresAt) || value.expiresAt < Date.now()) {
65
+ throw archiveOaValidationError('continuation_expired', 'continuation 已过期');
66
+ }
67
+ return value;
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,53 @@
1
+ import { CliError, ExitCode } from '../../core/errors.js';
2
+ export class ArchiveOaCliError extends CliError {
3
+ type;
4
+ subtype;
5
+ code;
6
+ retryable;
7
+ stage;
8
+ details;
9
+ partialData;
10
+ constructor(type, subtype, message, options = {}) {
11
+ super(message, options.exitCode ?? ExitCode.InternalError);
12
+ this.name = 'ArchiveOaCliError';
13
+ this.type = type;
14
+ this.subtype = subtype;
15
+ this.code = options.code;
16
+ this.retryable = options.retryable ?? false;
17
+ this.stage = options.stage;
18
+ this.details = options.details;
19
+ this.partialData = options.partialData;
20
+ }
21
+ }
22
+ export function archiveOaValidationError(subtype, message, details) {
23
+ return new ArchiveOaCliError('validation', subtype, message, {
24
+ details,
25
+ exitCode: ExitCode.UsageError,
26
+ });
27
+ }
28
+ export function archiveOaAuthError(subtype, message, details) {
29
+ return new ArchiveOaCliError('authentication', subtype, message, {
30
+ details,
31
+ exitCode: ExitCode.AuthError,
32
+ });
33
+ }
34
+ export function archiveOaNetworkError(subtype, message, options = {}) {
35
+ return new ArchiveOaCliError('network', subtype, message, {
36
+ ...options,
37
+ retryable: options.retryable ?? true,
38
+ exitCode: ExitCode.NetworkError,
39
+ });
40
+ }
41
+ export function archiveOaConfirmationError(action, details) {
42
+ return new ArchiveOaCliError('confirmation', 'required', '该操作需要明确确认', {
43
+ details: { action, ...details },
44
+ exitCode: ExitCode.ConfirmationRequired,
45
+ });
46
+ }
47
+ export function archiveOaPartialError(message, partialData, details) {
48
+ return new ArchiveOaCliError('partial', 'write_uncertain', message, {
49
+ details,
50
+ partialData,
51
+ exitCode: ExitCode.Partial,
52
+ });
53
+ }
@@ -0,0 +1,158 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
2
+ import { requireE10Session } from '../../internal/e10/context.js';
3
+ import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
+ import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
5
+ import { ArchiveOaCliError, archiveOaAuthError, archiveOaNetworkError, archiveOaValidationError, } from './errors.js';
6
+ /** 本模块允许调用的路径前缀:ebuilder 业务接口与 OpenESB 开放服务。 */
7
+ const ALLOWED_PATH_PREFIXES = ['/api/', '/openserver/'];
8
+ function inferProfile(ctx) {
9
+ const options = ctx.getGlobalOptions();
10
+ return options.profile || getActiveProfile();
11
+ }
12
+ /**
13
+ * 业务成功判定。
14
+ * - ebuilder 列表/ESB 接口:`{ code: 200, status: true }`
15
+ * - ebuilder 自定义接口(getFormDataList):`{ datajson: { status: true } }`
16
+ * - OpenESB:`{ errcode: "0" }`
17
+ */
18
+ export function isArchiveOaBusinessSuccess(value) {
19
+ if (!value || typeof value !== 'object')
20
+ return false;
21
+ if (value.errcode !== undefined && value.errcode !== null)
22
+ return String(value.errcode) === '0';
23
+ if (typeof value.status === 'boolean')
24
+ return value.status;
25
+ if (typeof value.success === 'boolean')
26
+ return value.success;
27
+ if (value.datajson && typeof value.datajson === 'object' && typeof value.datajson.status === 'boolean') {
28
+ return value.datajson.status;
29
+ }
30
+ if (value.code !== undefined && value.code !== null)
31
+ return Number(value.code) === 200;
32
+ return false;
33
+ }
34
+ export function archiveOaBusinessMessage(value) {
35
+ if (!value || typeof value !== 'object')
36
+ return 'OA 档案接口返回业务失败';
37
+ const candidates = [
38
+ value.errmsg,
39
+ value.msg,
40
+ value.message,
41
+ value.errMsg,
42
+ value.data?.msg,
43
+ value.data?.message,
44
+ value.datajson?.msg,
45
+ value.datajson?.message,
46
+ ];
47
+ for (const item of candidates) {
48
+ if (typeof item === 'string' && item.trim())
49
+ return item;
50
+ }
51
+ return 'OA 档案接口返回业务失败';
52
+ }
53
+ function archiveOaBusinessCode(value) {
54
+ if (!value || typeof value !== 'object')
55
+ return undefined;
56
+ const code = value.errcode ?? value.code ?? value.status;
57
+ if (typeof code === 'string' || typeof code === 'number')
58
+ return code;
59
+ return undefined;
60
+ }
61
+ export function assertArchiveOaPath(requestPath) {
62
+ if (typeof requestPath !== 'string' || !requestPath.startsWith('/') || requestPath.startsWith('//')) {
63
+ throw archiveOaValidationError('path_invalid', '请求路径必须是相对绝对路径');
64
+ }
65
+ if (/^https?:/i.test(requestPath) || /[\r\n]/.test(requestPath) || requestPath.includes('/../')) {
66
+ throw archiveOaValidationError('path_invalid', '请求路径不允许跨域或路径逃逸');
67
+ }
68
+ if (!ALLOWED_PATH_PREFIXES.some((prefix) => requestPath.startsWith(prefix))) {
69
+ throw archiveOaValidationError('path_not_allowed', `archive-oa 只能调用 ${ALLOWED_PATH_PREFIXES.join(' / ')} 路径`);
70
+ }
71
+ }
72
+ function omitEmptyQueryValues(query) {
73
+ if (!query)
74
+ return undefined;
75
+ return Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined && value !== null && value !== ''));
76
+ }
77
+ function mapRuntimeNetworkError(error) {
78
+ if (error instanceof E10ApiError && error.type === 'network') {
79
+ throw archiveOaNetworkError(error.subtype, error.message, {
80
+ retryable: error.retryable,
81
+ details: error.details,
82
+ });
83
+ }
84
+ throw error;
85
+ }
86
+ export function createArchiveOaHost(ctx, dependencies = {}) {
87
+ let e10Session = null;
88
+ let sessionContext = null;
89
+ const requireSession = dependencies.requireSession || requireE10Session;
90
+ async function ensureSession() {
91
+ if (!e10Session) {
92
+ e10Session = await requireSession(ctx);
93
+ sessionContext = {
94
+ baseUrl: e10Session.getBaseUrl(),
95
+ profile: inferProfile(ctx),
96
+ userId: e10Session.userId,
97
+ tenantKey: e10Session.tenantKey,
98
+ eteamsId: e10Session.eteamsId,
99
+ };
100
+ }
101
+ return e10Session;
102
+ }
103
+ async function getSession() {
104
+ await ensureSession();
105
+ return sessionContext;
106
+ }
107
+ async function request(request) {
108
+ const { method, path, query, body, operation, headers: extraHeaders = {}, timeoutMs = 60_000, skipBusinessCheck = false, } = request;
109
+ assertArchiveOaPath(path);
110
+ const resolvedMethod = method || (body === undefined ? 'GET' : 'POST');
111
+ const runtime = createE10RequestRuntime(ctx, await ensureSession(), {
112
+ interceptors: dependencies.interceptors,
113
+ fetch: dependencies.fetch,
114
+ });
115
+ const response = await runtime.requestJson({
116
+ module: 'archive-oa',
117
+ operation,
118
+ method: resolvedMethod,
119
+ path,
120
+ query: omitEmptyQueryValues(query),
121
+ body,
122
+ headers: { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders },
123
+ timeoutMs,
124
+ }).catch((error) => mapRuntimeNetworkError(error));
125
+ if (response.status === 401 || response.status === 302) {
126
+ throw archiveOaAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
127
+ }
128
+ if (response.parseError) {
129
+ if (response.status < 200 || response.status >= 300) {
130
+ throw new ArchiveOaCliError('api', 'http_error', `OA 档案接口 HTTP ${response.status}`, {
131
+ code: response.status,
132
+ details: { status: response.status },
133
+ exitCode: 1,
134
+ });
135
+ }
136
+ throw archiveOaNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
137
+ }
138
+ if (response.status < 200 || response.status >= 300) {
139
+ throw new ArchiveOaCliError('api', 'http_error', `OA 档案接口 HTTP ${response.status}`, {
140
+ code: response.status,
141
+ details: { status: response.status, message: archiveOaBusinessMessage(response.value) },
142
+ exitCode: 1,
143
+ });
144
+ }
145
+ if (!skipBusinessCheck && !isArchiveOaBusinessSuccess(response.value)) {
146
+ throw new ArchiveOaCliError('api', 'business_error', archiveOaBusinessMessage(response.value), {
147
+ code: archiveOaBusinessCode(response.value),
148
+ details: {
149
+ code: archiveOaBusinessCode(response.value),
150
+ message: archiveOaBusinessMessage(response.value),
151
+ },
152
+ exitCode: 1,
153
+ });
154
+ }
155
+ return response.value;
156
+ }
157
+ return { request, getSession };
158
+ }