@fa-software/semi2-dsh 0.0.18 → 0.0.19-preview.20261001161030

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 (67) hide show
  1. package/lib/api/bootstrap.js +45 -0
  2. package/lib/api/generated/http.js +1 -0
  3. package/lib/api/http-assets.js +89 -0
  4. package/lib/api/http-models.js +101 -0
  5. package/lib/api/http.js +99 -0
  6. package/lib/api/index.js +1 -72
  7. package/lib/api/login.js +3 -10
  8. package/lib/api/operations.js +9 -0
  9. package/lib/api/rest.js +57 -0
  10. package/lib/client.js.map +1 -1
  11. package/lib/cnb-token.js +37 -0
  12. package/lib/domains/default-model/domain.js +1 -3
  13. package/lib/domains/models/domain.js +3 -5
  14. package/lib/domains/models/web-search.js +2 -4
  15. package/lib/domains/project-profile/domain.js +1 -3
  16. package/lib/domains/project-profile/index.js +2 -2
  17. package/lib/domains/redmine/domain.js +1 -4
  18. package/lib/domains/redmine/index.js +2 -2
  19. package/lib/domains/secrets/domain.js +2 -6
  20. package/lib/domains/self-portrait/domain.js +1 -3
  21. package/lib/domains/self-portrait/index.js +1 -1
  22. package/lib/domains/skills/index.js +1 -1
  23. package/lib/domains/skills/mcp-resources.js +8 -10
  24. package/lib/domains/skills/provider.js +4 -4
  25. package/lib/domains/workspace/domain.js +3 -3
  26. package/lib/domains/workspace/feed.js +1 -1
  27. package/lib/domains/workspace/index.js +1 -1
  28. package/lib/domains/workspace/resolve.js +2 -4
  29. package/lib/hub/compile.js +5 -17
  30. package/lib/hub/credentials.js +3 -8
  31. package/lib/hub/errors.js +1 -3
  32. package/lib/hub/status.js +1 -1
  33. package/lib/hub/workspace-book.js +2 -6
  34. package/lib/index.js +3 -2
  35. package/lib/service/config-service.js +3 -4
  36. package/lib/service/connection.js +1 -1
  37. package/lib/service/lifecycle.js +3 -3
  38. package/lib/service/orchestrator.js +2 -4
  39. package/lib/service/ports.js +4 -4
  40. package/lib/service/registry.js +2 -8
  41. package/lib/service/sessions.js +5 -10
  42. package/lib/service/settings-tx.js +3 -6
  43. package/lib/service/status.js +4 -8
  44. package/lib/types/api/bootstrap.d.ts +23 -0
  45. package/lib/types/api/facts.d.ts +41 -0
  46. package/lib/types/api/generated/http.d.ts +3924 -0
  47. package/lib/types/api/http-assets.d.ts +65 -0
  48. package/lib/types/api/http-models.d.ts +68 -0
  49. package/lib/types/api/http.d.ts +16 -0
  50. package/lib/types/api/index.d.ts +10 -31
  51. package/lib/types/api/operations.d.ts +27 -0
  52. package/lib/types/api/rest.d.ts +3 -0
  53. package/lib/types/cnb-token.d.ts +24 -0
  54. package/lib/types/domains/project-profile/domain.d.ts +1 -1
  55. package/lib/types/hub/compile.d.ts +5 -5
  56. package/lib/types/index.d.ts +7 -5
  57. package/lib/types/service/config-service.d.ts +1 -1
  58. package/lib/web/action-routes.js +1 -3
  59. package/lib/web/index.js +2 -2
  60. package/lib/web/welcome-ack.js +6 -5
  61. package/package.json +3 -5
  62. package/lib/api/client.js +0 -119
  63. package/lib/api/generated/documents.js +0 -125
  64. package/lib/types/api/client.d.ts +0 -29
  65. package/lib/types/api/generated/documents.d.ts +0 -15
  66. package/lib/types/api/generated/graphql.d.ts +0 -184
  67. /package/lib/api/{generated/graphql.js → facts.js} +0 -0
@@ -0,0 +1,45 @@
1
+ import { ConfigurationError } from "../hub/errors.js";
2
+ import { array, decodeModelConfiguration } from "./http-models.js";
3
+ import { decodeSkill } from "./http-assets.js";
4
+ function object(value) {
5
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
6
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
7
+ return Object.fromEntries(Object.entries(value));
8
+ }
9
+ function text(value) {
10
+ if (typeof value !== 'string')
11
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
12
+ return value;
13
+ }
14
+ function nullableText(value) {
15
+ return value === null ? null : text(value);
16
+ }
17
+ /** 失败块不得伪装成空配置;缺块或未知状态说明协议不完整。 */
18
+ export function decodeBlock(value, decode) {
19
+ const block = object(value);
20
+ if (block.status === 'error') {
21
+ if (typeof block.code !== 'string' || !/^[A-Z][A-Z0-9_]{0,99}$/.test(block.code))
22
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
23
+ return { status: 'error', code: block.code };
24
+ }
25
+ if (block.status !== 'ok' || !Object.hasOwn(block, 'data'))
26
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
27
+ return { status: 'ok', data: decode(block.data) };
28
+ }
29
+ export function decodeBootstrap(value) {
30
+ const root = object(value);
31
+ const viewer = object(root.viewer);
32
+ const id = text(viewer.id);
33
+ if (!id)
34
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
35
+ return {
36
+ viewer: { id, username: text(viewer.username), displayName: text(viewer.displayName) },
37
+ models: decodeBlock(root.models, decodeModelConfiguration),
38
+ skills: decodeBlock(root.skills, data => array(data, decodeSkill)),
39
+ selfPortrait: decodeBlock(root.selfPortrait, text),
40
+ redmine: decodeBlock(root.redmine, data => {
41
+ const fact = object(data);
42
+ return { baseUrl: nullableText(fact.baseUrl), apiKey: nullableText(fact.apiKey) };
43
+ }),
44
+ };
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { ConfigurationError } from "../hub/errors.js";
2
+ import { array, boolean, integer, nullableString, reasoning, record, string } from "./http-models.js";
3
+ export function decodeFile(value) {
4
+ const file = record(value);
5
+ return {
6
+ path: string(file.path),
7
+ sizeBytes: integer(file.sizeBytes),
8
+ mimeType: string(file.mimeType),
9
+ isExecutable: boolean(file.isExecutable),
10
+ };
11
+ }
12
+ export function decodeFileContent(value) {
13
+ const file = record(value);
14
+ const encoding = file.encoding === 'UTF8' || file.encoding === 'utf-8'
15
+ ? 'UTF8'
16
+ : file.encoding === 'BASE64' || file.encoding === 'base64'
17
+ ? 'BASE64'
18
+ : null;
19
+ if (encoding === null)
20
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
21
+ return { ...decodeFile(file), content: string(file.content), encoding };
22
+ }
23
+ export function decodeSkill(value) {
24
+ const skill = record(value), invocation = record(skill.invocation);
25
+ return {
26
+ id: string(skill.id),
27
+ projectId: nullableString(skill.projectId),
28
+ name: string(skill.name),
29
+ description: string(skill.description),
30
+ whenToUse: nullableString(skill.whenToUse),
31
+ invocation: { modelInvocable: boolean(invocation.modelInvocable), userInvocable: boolean(invocation.userInvocable) },
32
+ metadata: skill.metadata === null ? null : record(skill.metadata),
33
+ content: string(skill.content),
34
+ version: integer(skill.version),
35
+ files: array(skill.files, decodeFile),
36
+ fileCount: integer(skill.fileCount),
37
+ filesBytes: integer(skill.filesBytes),
38
+ };
39
+ }
40
+ export function decodeProject(value) {
41
+ const project = record(value);
42
+ return {
43
+ id: string(project.id),
44
+ slug: string(project.slug),
45
+ name: string(project.name),
46
+ enabled: boolean(project.enabled),
47
+ planModelId: nullableString(project.planModelId),
48
+ executeModelId: nullableString(project.executeModelId),
49
+ planReasoningLevel: reasoning(project.planReasoningLevel),
50
+ executeReasoningLevel: reasoning(project.executeReasoningLevel),
51
+ };
52
+ }
53
+ export function decodeProfile(value) {
54
+ const profile = record(value);
55
+ const project = profile.redmine === null ? null : record(profile.redmine);
56
+ return {
57
+ projectName: string(profile.projectName),
58
+ projectSlug: string(profile.projectSlug),
59
+ redmineProjectId: profile.redmineProjectId === null ? null : integer(profile.redmineProjectId),
60
+ redmine: project === null
61
+ ? null
62
+ : {
63
+ id: integer(project.id),
64
+ identifier: string(project.identifier),
65
+ name: string(project.name),
66
+ status: integer(project.status),
67
+ homepage: nullableString(project.homepage),
68
+ description: nullableString(project.description),
69
+ truncated: boolean(project.truncated),
70
+ customFields: array(project.customFields, field => {
71
+ const row = record(field);
72
+ return { name: string(row.name), value: string(row.value) };
73
+ }),
74
+ },
75
+ };
76
+ }
77
+ export function decodeEvent(value) {
78
+ const event = record(value);
79
+ const outcome = event.outcome;
80
+ if (outcome !== 'SUCCESS' && outcome !== 'DENIED' && outcome !== 'ERROR')
81
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
82
+ return {
83
+ id: string(event.id),
84
+ action: string(event.action),
85
+ outcome,
86
+ code: nullableString(event.code),
87
+ occurredAt: string(event.occurredAt),
88
+ };
89
+ }
@@ -0,0 +1,101 @@
1
+ import { ConfigurationError } from "../hub/errors.js";
2
+ export function record(value) {
3
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
4
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
5
+ return Object.fromEntries(Object.entries(value));
6
+ }
7
+ export function string(value) {
8
+ if (typeof value !== 'string')
9
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
10
+ return value;
11
+ }
12
+ export function nullableString(value) {
13
+ return value === null ? null : string(value);
14
+ }
15
+ export function array(value, decode) {
16
+ if (!Array.isArray(value))
17
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
18
+ return value.map((entry) => decode(entry));
19
+ }
20
+ export function boolean(value) {
21
+ if (typeof value !== 'boolean')
22
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
23
+ return value;
24
+ }
25
+ export function integer(value) {
26
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
27
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
28
+ return value;
29
+ }
30
+ export function protocol(value) {
31
+ if (value === 'DEEPSEEK' || value === 'OPENAI_CHAT' || value === 'OPENAI_RESPONSES' || value === 'ANTHROPIC')
32
+ return value;
33
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
34
+ }
35
+ export function reasoning(value) {
36
+ if (value === null || value === undefined)
37
+ return null;
38
+ const level = string(value).toUpperCase();
39
+ switch (level) {
40
+ case 'OFF':
41
+ case 'MINIMAL':
42
+ case 'LOW':
43
+ case 'MEDIUM':
44
+ case 'HIGH':
45
+ case 'XHIGH':
46
+ case 'MAX':
47
+ return level;
48
+ default:
49
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
50
+ }
51
+ }
52
+ function modality(value) {
53
+ const mode = string(value).toUpperCase();
54
+ if (mode === 'TEXT' || mode === 'IMAGE')
55
+ return mode;
56
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
57
+ }
58
+ export function decodeModel(value) {
59
+ const model = record(value), connection = record(model.connection), metadata = record(model.metadata);
60
+ return {
61
+ id: string(model.id),
62
+ upstreamId: string(model.upstreamId),
63
+ displayName: nullableString(model.displayName),
64
+ defaultReasoningLevel: reasoning(model.defaultReasoningLevel),
65
+ connection: {
66
+ id: string(connection.id),
67
+ provider: nullableString(connection.provider),
68
+ protocol: protocol(connection.protocol),
69
+ baseUrl: string(connection.baseUrl),
70
+ },
71
+ metadata: {
72
+ contextWindow: metadata.contextWindow == null ? null : integer(metadata.contextWindow),
73
+ maxTokens: metadata.maxTokens == null ? null : integer(metadata.maxTokens),
74
+ input: metadata.input == null ? null : array(metadata.input, modality),
75
+ reasoningLevels: metadata.reasoningLevels == null
76
+ ? null
77
+ : array(metadata.reasoningLevels, entry => {
78
+ const level = reasoning(entry);
79
+ if (level === null)
80
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
81
+ return level;
82
+ }),
83
+ },
84
+ };
85
+ }
86
+ export function decodeCredential(value) {
87
+ const credential = record(value);
88
+ return { connectionId: string(credential.connectionId), value: string(credential.value) };
89
+ }
90
+ export function decodeModelConfiguration(value) {
91
+ const config = record(value), defaults = record(config.defaults);
92
+ const selection = (value) => {
93
+ const selected = record(value);
94
+ return { modelId: nullableString(selected.modelId), reasoningLevel: reasoning(selected.reasoningLevel) };
95
+ };
96
+ return {
97
+ models: array(config.models, decodeModel),
98
+ credentials: array(config.credentials, decodeCredential),
99
+ defaults: { web: selection(defaults.web), plan: selection(defaults.plan), execute: selection(defaults.execute) },
100
+ };
101
+ }
@@ -0,0 +1,99 @@
1
+ import { ConfigurationError } from "../hub/errors.js";
2
+ const TOKEN = /^(?:cnbk_[A-Za-z0-9_-]+|[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
3
+ const MAX_BYTES = 2 * 1024 * 1024;
4
+ /** 保留部署路径前缀;操作路径只能由代码提供,不能跨 origin 或向上跳转。 */
5
+ export function runtimeEndpoint(baseUrl, path, nodeEnv) {
6
+ let base;
7
+ try {
8
+ base = new URL(baseUrl.trim());
9
+ }
10
+ catch {
11
+ throw new ConfigurationError('INVALID_CONFIG_URL');
12
+ }
13
+ const loopback = base.hostname === 'localhost' || base.hostname === '[::1]' || /^127(?:\.\d{1,3}){3}$/.test(base.hostname);
14
+ if (base.username ||
15
+ base.password ||
16
+ base.search ||
17
+ base.hash ||
18
+ (base.protocol !== 'https:' && !(base.protocol === 'http:' && nodeEnv !== 'production' && loopback)))
19
+ throw new ConfigurationError('INVALID_CONFIG_URL');
20
+ if (!/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(path))
21
+ throw new ConfigurationError('INVALID_INPUT');
22
+ base.pathname = `${base.pathname.replace(/\/+$/, '')}/api/v1/runtime/${path}`;
23
+ return base;
24
+ }
25
+ /** 不信任远端 DTO:返回 unknown,由每个业务入口完成结构校验。 */
26
+ export async function requestRuntime(options, path, body) {
27
+ const endpoint = runtimeEndpoint(options.baseUrl, path, options.nodeEnv);
28
+ const token = options.token.trim();
29
+ if (token.length > 512 || !TOKEN.test(token))
30
+ throw new ConfigurationError('MISSING_ACCESS_TOKEN');
31
+ try {
32
+ const response = await (options.fetchImpl ?? fetch)(endpoint, {
33
+ method: body === undefined ? 'GET' : 'POST',
34
+ headers: {
35
+ authorization: `Bearer ${token}`,
36
+ accept: 'application/json',
37
+ 'cache-control': 'no-store',
38
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
39
+ ...(options.creator === undefined ? {} : { 'x-semi2-cnb-creator': options.creator }),
40
+ ...(options.channel === undefined ? {} : { 'x-semi2-channel': options.channel }),
41
+ },
42
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
43
+ redirect: 'error',
44
+ cache: 'no-store',
45
+ signal: AbortSignal.timeout(15_000),
46
+ });
47
+ if (!response.body)
48
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
49
+ const reader = response.body.getReader();
50
+ const chunks = [];
51
+ let size = 0;
52
+ try {
53
+ for (;;) {
54
+ const next = await reader.read();
55
+ if (next.done)
56
+ break;
57
+ const chunk = next.value;
58
+ if (!(chunk instanceof Uint8Array))
59
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
60
+ size += chunk.byteLength;
61
+ if (size > MAX_BYTES) {
62
+ await reader.cancel();
63
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
64
+ }
65
+ chunks.push(chunk);
66
+ }
67
+ }
68
+ finally {
69
+ reader.releaseLock();
70
+ }
71
+ let data;
72
+ try {
73
+ data = JSON.parse(Buffer.concat(chunks).toString('utf8'));
74
+ }
75
+ catch {
76
+ if (response.status === 401)
77
+ return { data: null, errorCode: 'UNAUTHENTICATED' };
78
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
79
+ }
80
+ if (!response.ok) {
81
+ const code = typeof data === 'object' &&
82
+ data !== null &&
83
+ 'code' in data &&
84
+ typeof data.code === 'string' &&
85
+ /^[A-Z][A-Z0-9_]{0,99}$/.test(data.code)
86
+ ? data.code
87
+ : response.status === 401
88
+ ? 'UNAUTHENTICATED'
89
+ : 'CONFIG_REQUEST_FAILED';
90
+ return { data: null, errorCode: code };
91
+ }
92
+ return { data, errorCode: null };
93
+ }
94
+ catch (error) {
95
+ if (error instanceof ConfigurationError)
96
+ throw error;
97
+ throw new ConfigurationError('CONFIG_REQUEST_FAILED');
98
+ }
99
+ }
package/lib/api/index.js CHANGED
@@ -1,72 +1 @@
1
- import { consoleEndpoint, postGraphql } from "./client.js";
2
- import { RUNTIME_REPORT_EVENT_DOCUMENT, RUNTIME_BOOT_DOCUMENT, RUNTIME_MODEL_CREDENTIALS_DOCUMENT, RUNTIME_PROJECT_PROFILE_DOCUMENT, RUNTIME_SKILL_FILES_DOCUMENT, RUNTIME_WORKSPACE_PROJECTS_DOCUMENT, } from "./generated/documents.js";
3
- import { exchangeCnbToken, loginToConsole } from "./login.js";
4
- import { ConfigurationError } from "../hub/errors.js";
5
- function text(value) {
6
- const trimmed = value?.trim();
7
- return trimmed === undefined || trimmed === '' ? undefined : trimmed;
8
- }
9
- export function createSemi2Api(options) {
10
- const request = async (operationName, document, variables = {}) => {
11
- const connection = options.readConnection();
12
- if (connection === null)
13
- throw new ConfigurationError('MISSING_ACCESS_TOKEN');
14
- // CNB 端带上环境创建者:hub 用它决定读取身份(密钥只证明来自 CNB 环境)。
15
- const creator = text(options.environment.CNB_BUILD_USER_ID);
16
- const outcome = await postGraphql({
17
- endpoint: consoleEndpoint(connection.url, options.environment.NODE_ENV),
18
- token: connection.token,
19
- ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
20
- ...(creator === undefined ? {} : { creator }),
21
- ...(() => {
22
- const channel = options.readChannel?.();
23
- return channel === undefined ? {} : { channel };
24
- })(),
25
- }, {
26
- operationName,
27
- document,
28
- ...(Object.keys(variables).length === 0 ? {} : { variables }),
29
- });
30
- return { data: outcome.data, errorCode: outcome.errorCode };
31
- };
32
- return {
33
- boot: () => request('RuntimeBoot', RUNTIME_BOOT_DOCUMENT),
34
- workspaceProjects: slugs => request('RuntimeWorkspaceProjects', RUNTIME_WORKSPACE_PROJECTS_DOCUMENT, {
35
- slugs,
36
- }),
37
- projectProfile: projectId => request('RuntimeProjectProfile', RUNTIME_PROJECT_PROFILE_DOCUMENT, {
38
- projectId,
39
- }),
40
- skillFiles: skillId => request('RuntimeSkillFiles', RUNTIME_SKILL_FILES_DOCUMENT, { skillId }),
41
- modelCredentials: connectionIds => request('RuntimeModelCredentials', RUNTIME_MODEL_CREDENTIALS_DOCUMENT, connectionIds === undefined ? {} : { connectionIds }),
42
- exchangeCnbToken: input => exchangeCnbToken(input, options.fetchImpl),
43
- loginToConsole: input => loginToConsole(input, options.fetchImpl),
44
- reportEvent: event => request('RuntimeReportEvent', RUNTIME_REPORT_EVENT_DOCUMENT, {
45
- input: {
46
- action: event.action,
47
- outcome: event.outcome,
48
- ...(event.code === undefined ? {} : { code: event.code }),
49
- ...(event.projectSlug === undefined
50
- ? {}
51
- : { projectSlug: event.projectSlug }),
52
- ...(event.targetLabel === undefined
53
- ? {}
54
- : { targetLabel: event.targetLabel }),
55
- ...(event.detail === undefined ? {} : { detail: event.detail }),
56
- ...(event.sessionId === undefined ? {} : { sessionId: event.sessionId }),
57
- ...(event.clientId === undefined ? {} : { clientId: event.clientId }),
58
- ...(event.runRef === undefined ? {} : { runRef: event.runRef }),
59
- ...(event.workspacePath === undefined
60
- ? {}
61
- : { workspacePath: event.workspacePath }),
62
- ...(event.workspaceSlug === undefined
63
- ? {}
64
- : { workspaceSlug: event.workspaceSlug }),
65
- ...(event.modelId === undefined ? {} : { modelId: event.modelId }),
66
- ...(event.reasoningLevel === undefined
67
- ? {}
68
- : { reasoningLevel: event.reasoningLevel }),
69
- },
70
- }),
71
- };
72
- }
1
+ export { createRestApi as createSemi2Api } from "./rest.js";
package/lib/api/login.js CHANGED
@@ -12,17 +12,12 @@ export function consoleOrigin(value, nodeEnv) {
12
12
  catch {
13
13
  throw new ConfigurationError('INVALID_CONFIG_URL');
14
14
  }
15
- const loopback = endpoint.hostname === 'localhost' ||
16
- endpoint.hostname === '[::1]' ||
17
- /^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname);
15
+ const loopback = endpoint.hostname === 'localhost' || endpoint.hostname === '[::1]' || /^127(?:\.\d{1,3}){3}$/.test(endpoint.hostname);
18
16
  if (endpoint.username ||
19
17
  endpoint.password ||
20
18
  endpoint.search ||
21
19
  endpoint.hash ||
22
- (endpoint.protocol !== 'https:' &&
23
- !(endpoint.protocol === 'http:' &&
24
- nodeEnv !== 'production' &&
25
- loopback)))
20
+ (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:' && nodeEnv !== 'production' && loopback)))
26
21
  throw new ConfigurationError('INVALID_CONFIG_URL');
27
22
  return endpoint.origin;
28
23
  }
@@ -64,9 +59,7 @@ export async function loginToConsole(input, fetchImpl = fetch) {
64
59
  const token = payload?.token;
65
60
  if (typeof token !== 'string' || !USER_KEY_SHAPE.test(token))
66
61
  throw new ConfigurationError('LOGIN_FAILED');
67
- const userName = typeof payload?.user?.username === 'string'
68
- ? payload.user.username
69
- : '';
62
+ const userName = typeof payload?.user?.username === 'string' ? payload.user.username : '';
70
63
  return { userName, token, url: origin };
71
64
  }
72
65
  /** CNB 运行端:平台注入的短期 CNB_TOKEN 换 hub 用户密钥(服务端到服务端,无 Origin/CSRF)。 */
@@ -0,0 +1,9 @@
1
+ /** 领域操作名与 HTTP 契约 operationId 对应;路径由实际请求适配器使用。 */
2
+ export const RUNTIME_OPERATIONS = {
3
+ RuntimeBoot: { operationId: 'runtimeBootstrap', path: 'bootstrap' },
4
+ RuntimeWorkspaceProjects: { operationId: 'runtimeProjects', path: 'projects/query' },
5
+ RuntimeModelCredentials: { operationId: 'runtimeCredentials', path: 'model-credentials/query' },
6
+ RuntimeSkillFiles: { operationId: 'runtimeSkillFiles', path: 'skills' },
7
+ RuntimeProjectProfile: { operationId: 'runtimeProjectProfile', path: 'projects' },
8
+ RuntimeReportEvent: { operationId: 'runtimeReportEvent', path: 'events' },
9
+ };
@@ -0,0 +1,57 @@
1
+ import { ConfigurationError } from "../hub/errors.js";
2
+ import { requestRuntime } from "./http.js";
3
+ import { decodeBootstrap } from "./bootstrap.js";
4
+ import { array, decodeCredential } from "./http-models.js";
5
+ import { decodeEvent, decodeFileContent, decodeProfile, decodeProject } from "./http-assets.js";
6
+ import { exchangeCnbToken, loginToConsole } from "./login.js";
7
+ import { RUNTIME_OPERATIONS as operations } from "./operations.js";
8
+ /** 六个业务读取入口,不把服务端资源路径泄漏进领域编排。 */
9
+ export function createRestApi(options) {
10
+ async function request(path, decode, body) {
11
+ const connection = options.readConnection();
12
+ if (connection === null)
13
+ throw new ConfigurationError('MISSING_ACCESS_TOKEN');
14
+ const creator = options.environment.CNB_BUILD_USER_ID?.trim();
15
+ const channel = options.readChannel?.();
16
+ const result = await requestRuntime({
17
+ baseUrl: connection.url,
18
+ token: connection.token,
19
+ ...(options.environment.NODE_ENV === undefined ? {} : { nodeEnv: options.environment.NODE_ENV }),
20
+ ...(creator ? { creator } : {}),
21
+ ...(channel === undefined ? {} : { channel }),
22
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
23
+ }, path, body);
24
+ if (result.errorCode !== null)
25
+ return { data: null, errorCode: result.errorCode };
26
+ return { data: decode(result.data), errorCode: null };
27
+ }
28
+ return {
29
+ boot: async () => {
30
+ const result = await request(operations.RuntimeBoot.path, decodeBootstrap);
31
+ if (result.data === null)
32
+ return { data: null, errorCode: result.errorCode };
33
+ const boot = result.data;
34
+ const facts = {
35
+ runtimeViewer: boot.viewer,
36
+ runtimeModels: boot.models.status === 'ok' ? boot.models.data.models : null,
37
+ runtimeModelCredentials: boot.models.status === 'ok' ? boot.models.data.credentials : null,
38
+ runtimeDefaultModelId: boot.models.status === 'ok' ? boot.models.data.defaults.web.modelId : null,
39
+ runtimeHeadlessModels: boot.models.status === 'ok'
40
+ ? { planModelId: boot.models.data.defaults.plan.modelId, executeModelId: boot.models.data.defaults.execute.modelId }
41
+ : null,
42
+ runtimeSkillCatalog: boot.skills.status === 'ok' ? boot.skills.data : null,
43
+ runtimeSelfPortrait: boot.selfPortrait.status === 'ok' ? boot.selfPortrait.data : null,
44
+ runtimeRedmine: boot.redmine.status === 'ok' ? boot.redmine.data : null,
45
+ };
46
+ const failure = [boot.models, boot.skills, boot.selfPortrait, boot.redmine].find(block => block.status === 'error');
47
+ return { data: facts, errorCode: failure?.status === 'error' ? failure.code : null };
48
+ },
49
+ workspaceProjects: slugs => request(operations.RuntimeWorkspaceProjects.path, data => ({ runtimeProjects: array(data, decodeProject) }), { slugs }),
50
+ modelCredentials: connectionIds => request(operations.RuntimeModelCredentials.path, data => ({ runtimeModelCredentials: array(data, decodeCredential) }), connectionIds === undefined ? {} : { connectionIds }),
51
+ projectProfile: id => request(`${operations.RuntimeProjectProfile.path}/${id}/profile`, data => ({ runtimeProjectProfile: decodeProfile(data) })),
52
+ skillFiles: id => request(`${operations.RuntimeSkillFiles.path}/${id}/files`, data => ({ runtimeSkillFiles: array(data, decodeFileContent) })),
53
+ reportEvent: event => request(operations.RuntimeReportEvent.path, data => ({ runtimeReportEvent: decodeEvent(data) }), event),
54
+ exchangeCnbToken: input => exchangeCnbToken(input, options.fetchImpl),
55
+ loginToConsole: input => loginToConsole(input, options.fetchImpl),
56
+ };
57
+ }