create-principles-disciple 1.109.0 → 1.110.0

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 (78) hide show
  1. package/console/dist/server/config/pd-config-store.d.ts +29 -0
  2. package/console/dist/server/config/pd-config-store.js +55 -0
  3. package/console/dist/server/feedback/channels.d.ts +36 -0
  4. package/console/dist/server/feedback/channels.js +89 -0
  5. package/console/dist/server/feedback/github-adapter.d.ts +44 -0
  6. package/console/dist/server/feedback/github-adapter.js +125 -0
  7. package/console/dist/server/feedback/ingest-adapter.d.ts +45 -0
  8. package/console/dist/server/feedback/ingest-adapter.js +129 -0
  9. package/console/dist/server/feedback/submit-service.d.ts +44 -0
  10. package/console/dist/server/feedback/submit-service.js +177 -0
  11. package/console/dist/server/index.js +23 -7
  12. package/console/dist/server/models/FeedbackReportConsoleModel.d.ts +8 -0
  13. package/console/dist/server/models/FeedbackReportConsoleModel.js +45 -0
  14. package/console/dist/server/routes/feedback-reports.d.ts +24 -0
  15. package/console/dist/server/routes/feedback-reports.js +151 -0
  16. package/console/dist/ui/api.d.ts +11 -3
  17. package/console/dist/ui/api.js +25 -2
  18. package/console/dist/ui/i18n/en.json +48 -2
  19. package/console/dist/ui/i18n/zh-CN.json +48 -2
  20. package/console/dist/ui/pages/ReportProblemValidators.d.ts +30 -0
  21. package/console/dist/ui/pages/ReportProblemValidators.js +66 -0
  22. package/console/dist/ui/pages/activation/ActivationPage.js +14 -3
  23. package/console/dist/ui/pages/pain/PainPage.js +6 -1
  24. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +4 -1
  25. package/console/dist/ui/pages/report-problem/ReportProblemPage.js +263 -70
  26. package/console/dist/ui/utils/validators.d.ts +22 -0
  27. package/console/dist/ui/utils/validators.js +66 -0
  28. package/console/dist/web/assets/app.css +42 -0
  29. package/console/dist/web/assets/app.js +699 -127
  30. package/console/package.json +4 -4
  31. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +4 -0
  32. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  33. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  34. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  35. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts +2 -0
  36. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.d.ts.map +1 -0
  37. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js +126 -0
  38. package/core/dist/runtime-v2/feedback/__tests__/fingerprint.test.js.map +1 -0
  39. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts +2 -0
  40. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.d.ts.map +1 -0
  41. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js +278 -0
  42. package/core/dist/runtime-v2/feedback/__tests__/slice1-typed-fields.test.js.map +1 -0
  43. package/core/dist/runtime-v2/feedback/create-report.d.ts.map +1 -1
  44. package/core/dist/runtime-v2/feedback/create-report.js +38 -1
  45. package/core/dist/runtime-v2/feedback/create-report.js.map +1 -1
  46. package/core/dist/runtime-v2/feedback/feedback-types.d.ts +60 -0
  47. package/core/dist/runtime-v2/feedback/feedback-types.d.ts.map +1 -1
  48. package/core/dist/runtime-v2/feedback/feedback-types.js +58 -0
  49. package/core/dist/runtime-v2/feedback/feedback-types.js.map +1 -1
  50. package/core/dist/runtime-v2/feedback/fingerprint.d.ts +31 -0
  51. package/core/dist/runtime-v2/feedback/fingerprint.d.ts.map +1 -0
  52. package/core/dist/runtime-v2/feedback/fingerprint.js +51 -0
  53. package/core/dist/runtime-v2/feedback/fingerprint.js.map +1 -0
  54. package/core/dist/runtime-v2/feedback/index.d.ts +3 -2
  55. package/core/dist/runtime-v2/feedback/index.d.ts.map +1 -1
  56. package/core/dist/runtime-v2/feedback/index.js +2 -1
  57. package/core/dist/runtime-v2/feedback/index.js.map +1 -1
  58. package/core/dist/runtime-v2/feedback/privacy-preview.d.ts.map +1 -1
  59. package/core/dist/runtime-v2/feedback/privacy-preview.js +43 -0
  60. package/core/dist/runtime-v2/feedback/privacy-preview.js.map +1 -1
  61. package/core/dist/runtime-v2/feedback/render-markdown.d.ts.map +1 -1
  62. package/core/dist/runtime-v2/feedback/render-markdown.js +33 -0
  63. package/core/dist/runtime-v2/feedback/render-markdown.js.map +1 -1
  64. package/core/dist/runtime-v2/index.d.ts +2 -2
  65. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  66. package/core/dist/runtime-v2/index.js +1 -1
  67. package/core/dist/runtime-v2/index.js.map +1 -1
  68. package/dist/installer.d.ts.map +1 -1
  69. package/dist/installer.js +25 -1
  70. package/dist/installer.js.map +1 -1
  71. package/dist/mvp-config.d.ts +6 -1
  72. package/dist/mvp-config.d.ts.map +1 -1
  73. package/dist/mvp-config.js +20 -1
  74. package/dist/mvp-config.js.map +1 -1
  75. package/package.json +1 -1
  76. package/plugin/dist/bundle.js +302 -302
  77. package/plugin/openclaw.plugin.json +1 -1
  78. package/plugin/package.json +2 -2
@@ -140,6 +140,35 @@ export declare function getPrinciplesOutputLanguage(workspaceDir: string): Outpu
140
140
  * bypasses; uses Object.hasOwn for untrusted key checks (ERR-013).
141
141
  */
142
142
  export declare function getFeedbackMaintainerEmail(workspaceDir: string): string;
143
+ /**
144
+ * Channel-side parameters for the feedback submit ladder (§4 / §10 of
145
+ * docs/superpowers/specs/2026-08-17-feedback-last-mile-submit-design.md).
146
+ *
147
+ * All values are optional-from-config: an empty string means "key not set or
148
+ * not a string" → the corresponding channel reports unavailable. Presence of
149
+ * `ingest_url` enables the primary channel; presence of `github_repo` enables
150
+ * the gh CLI channel.
151
+ *
152
+ * Reads parse YAML as unknown (ERR-001/005: no `as` bypasses) and uses
153
+ * Object.hasOwn for untrusted key checks (ERR-013). Mirrors the
154
+ * `getFeedbackMaintainerEmail` pattern.
155
+ */
156
+ export type FeedbackChannelConfig = {
157
+ ingestUrl: string;
158
+ ingestToken: string;
159
+ githubRepo: string;
160
+ githubProxy: string;
161
+ };
162
+ /**
163
+ * Read `feedback.ingest_url` / `ingest_token` / `github_repo` / `github_proxy`
164
+ * from `.pd/config.yaml`. Missing/malformed config falls back to all-empty.
165
+ *
166
+ * NOTE: `ingest_token` is returned here but MUST NOT be serialised to any
167
+ * client-facing payload (channels endpoint omits it; only the server submit
168
+ * path uses it). It ships in the release and is an anti-abuse boundary, not a
169
+ * security boundary (spec §9.2).
170
+ */
171
+ export declare function getFeedbackChannelConfig(workspaceDir: string): FeedbackChannelConfig;
143
172
  /**
144
173
  * Update principles.outputLanguage in config.yaml.
145
174
  * Safe partial write: preserves unknown sections, validates before write.
@@ -579,6 +579,61 @@ export function getFeedbackMaintainerEmail(workspaceDir) {
579
579
  }
580
580
  return emailRaw;
581
581
  }
582
+ const EMPTY_FEEDBACK_CHANNEL = {
583
+ ingestUrl: '',
584
+ ingestToken: '',
585
+ githubRepo: '',
586
+ githubProxy: '',
587
+ };
588
+ /** Read a known string key from an already-validated record, or '' when absent/non-string. */
589
+ function readConfigString(record, key) {
590
+ if (!Object.hasOwn(record, key))
591
+ return '';
592
+ const v = record[key];
593
+ return typeof v === 'string' ? v : '';
594
+ }
595
+ /**
596
+ * Read `feedback.ingest_url` / `ingest_token` / `github_repo` / `github_proxy`
597
+ * from `.pd/config.yaml`. Missing/malformed config falls back to all-empty.
598
+ *
599
+ * NOTE: `ingest_token` is returned here but MUST NOT be serialised to any
600
+ * client-facing payload (channels endpoint omits it; only the server submit
601
+ * path uses it). It ships in the release and is an anti-abuse boundary, not a
602
+ * security boundary (spec §9.2).
603
+ */
604
+ export function getFeedbackChannelConfig(workspaceDir) {
605
+ const configPath = getPdConfigPath(workspaceDir);
606
+ if (!fs.existsSync(configPath)) {
607
+ return { ...EMPTY_FEEDBACK_CHANNEL };
608
+ }
609
+ let raw;
610
+ try {
611
+ raw = fs.readFileSync(configPath, 'utf8');
612
+ }
613
+ catch {
614
+ return { ...EMPTY_FEEDBACK_CHANNEL };
615
+ }
616
+ let parsed;
617
+ try {
618
+ parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
619
+ }
620
+ catch {
621
+ return { ...EMPTY_FEEDBACK_CHANNEL };
622
+ }
623
+ if (!isRecord(parsed)) {
624
+ return { ...EMPTY_FEEDBACK_CHANNEL };
625
+ }
626
+ const feedbackRaw = Object.hasOwn(parsed, 'feedback') ? parsed.feedback : undefined;
627
+ if (feedbackRaw === undefined || !isRecord(feedbackRaw)) {
628
+ return { ...EMPTY_FEEDBACK_CHANNEL };
629
+ }
630
+ return {
631
+ ingestUrl: readConfigString(feedbackRaw, 'ingest_url'),
632
+ ingestToken: readConfigString(feedbackRaw, 'ingest_token'),
633
+ githubRepo: readConfigString(feedbackRaw, 'github_repo'),
634
+ githubProxy: readConfigString(feedbackRaw, 'github_proxy'),
635
+ };
636
+ }
582
637
  /**
583
638
  * Update principles.outputLanguage in config.yaml.
584
639
  * Safe partial write: preserves unknown sections, validates before write.
@@ -0,0 +1,36 @@
1
+ import type * as childProcessNS from 'node:child_process';
2
+ import type * as fsNS from 'node:fs';
3
+ import type * as osNS from 'node:os';
4
+ import type * as pathNS from 'node:path';
5
+ import type { FeedbackChannelConfig } from '../config/pd-config-store.js';
6
+ type ExecFileFn = typeof childProcessNS.execFile;
7
+ export interface ChannelStatus {
8
+ id: 'ingest' | 'github' | 'email' | 'file';
9
+ available: boolean;
10
+ /** Human-readable reason when unavailable. */
11
+ reason?: string;
12
+ /** Concrete next action the user can take (rc-9). */
13
+ nextAction?: string;
14
+ }
15
+ export interface ChannelsResult {
16
+ ok: true;
17
+ channels: ChannelStatus[];
18
+ }
19
+ export declare function clearProbeCache(): void;
20
+ export type ChannelDeps = {
21
+ fs?: typeof fsNS;
22
+ os?: typeof osNS;
23
+ path?: typeof pathNS;
24
+ execFile?: ExecFileFn;
25
+ fetchFn?: typeof fetch;
26
+ probeTimeoutMs?: number;
27
+ };
28
+ /**
29
+ * Build the full channel ladder status for a workspace.
30
+ * `maintainerEmail` is supplied by the caller (already computed from config).
31
+ * `featureFlagEnabled` gates the whole submit area (spec §12: flag off hides it
32
+ * and the endpoint 403s); channels still probe but the caller may drop the
33
+ * area entirely when the flag is off.
34
+ */
35
+ export declare function probeChannels(config: FeedbackChannelConfig, deps: ChannelDeps, maintainerEmail?: string): Promise<ChannelsResult>;
36
+ export {};
@@ -0,0 +1,89 @@
1
+ // channels.ts
2
+ // Channel-ladder availability probing for feedback submission (PRI-543).
3
+ //
4
+ // Spec §4 / §8.1: the UI orders channels by ladder position and disables
5
+ // unavailable ones, showing reason + nextAction (rc-9). Probing is fail-open:
6
+ // inability to probe is reported as available so a transient probe failure
7
+ // never hides a channel; the submit path then fails loudly and surfaces a
8
+ // structured reason + downgrade hint.
9
+ //
10
+ // Probing results are cached per (channel,url-or-repo) for 60s to avoid
11
+ // hammering the relay /shelling out to gh on every list/via fetch.
12
+ import { probeIngestHealth } from './ingest-adapter.js';
13
+ import { detectGithubCli } from './github-adapter.js';
14
+ const PROBE_CACHE_TTL_MS = 60_000;
15
+ const probeCache = new Map();
16
+ function cachedOrCompute(key, compute) {
17
+ const hit = probeCache.get(key);
18
+ if (hit && Date.now() - hit.at < PROBE_CACHE_TTL_MS) {
19
+ return Promise.resolve(hit);
20
+ }
21
+ return compute().then((entry) => {
22
+ probeCache.set(key, entry);
23
+ return entry;
24
+ });
25
+ }
26
+ export function clearProbeCache() {
27
+ probeCache.clear();
28
+ }
29
+ /**
30
+ * Build the full channel ladder status for a workspace.
31
+ * `maintainerEmail` is supplied by the caller (already computed from config).
32
+ * `featureFlagEnabled` gates the whole submit area (spec §12: flag off hides it
33
+ * and the endpoint 403s); channels still probe but the caller may drop the
34
+ * area entirely when the flag is off.
35
+ */
36
+ export async function probeChannels(config, deps, maintainerEmail = '') {
37
+ const channels = [];
38
+ // 1. ingest — present iff ingest_url is configured; live-probed against /health.
39
+ if (config.ingestUrl.length === 0) {
40
+ channels.push({
41
+ id: 'ingest',
42
+ available: false,
43
+ reason: 'ingest 通道未配置',
44
+ nextAction: '在 .pd/config.yaml 配置 feedback.ingest_url 后重启 console',
45
+ });
46
+ }
47
+ else {
48
+ const entry = await cachedOrCompute(`ingest:${config.ingestUrl}`, () => probeIngestHealth(config.ingestUrl, { fetchFn: deps.fetchFn, timeoutMs: deps.probeTimeoutMs }).then((r) => ({
49
+ available: r.available,
50
+ reason: r.reason,
51
+ nextAction: r.nextAction,
52
+ at: Date.now(),
53
+ })));
54
+ channels.push({ id: 'ingest', available: entry.available, reason: entry.reason, nextAction: entry.nextAction });
55
+ }
56
+ // 2. github — present iff github_repo configured; gh CLI must exist + be authed.
57
+ if (config.githubRepo.length === 0) {
58
+ channels.push({
59
+ id: 'github',
60
+ available: false,
61
+ reason: 'gh 通道未配置',
62
+ nextAction: '在 .pd/config.yaml 配置 feedback.github_repo 后重启 console',
63
+ });
64
+ }
65
+ else {
66
+ const entry = await cachedOrCompute(`github:${config.githubRepo}`, () => detectGithubCli(deps).then((r) => ({
67
+ available: r.available,
68
+ reason: r.reason,
69
+ nextAction: r.nextAction,
70
+ at: Date.now(),
71
+ })));
72
+ channels.push({ id: 'github', available: entry.available, reason: entry.reason, nextAction: entry.nextAction });
73
+ }
74
+ // 3. email — always depends on a real maintainer email (never the placeholder).
75
+ if (maintainerEmail.length === 0 || maintainerEmail === 'maintainer@example.com') {
76
+ channels.push({
77
+ id: 'email',
78
+ available: false,
79
+ reason: '维护者邮箱未配置',
80
+ nextAction: '在 .pd/config.yaml 设置 feedback.maintainer_email 后重启 console',
81
+ });
82
+ }
83
+ else {
84
+ channels.push({ id: 'email', available: true });
85
+ }
86
+ // 4. file — always available (offline export).
87
+ channels.push({ id: 'file', available: true });
88
+ return { ok: true, channels };
89
+ }
@@ -0,0 +1,44 @@
1
+ import type * as childProcessNS from 'node:child_process';
2
+ import type * as fsNS from 'node:fs';
3
+ import type * as osNS from 'node:os';
4
+ import type * as pathNS from 'node:path';
5
+ import type { FeedbackReport } from '@principles/core/runtime-v2/feedback';
6
+ import type { ChannelDeps } from './channels.js';
7
+ type ExecFileFn = typeof childProcessNS.execFile;
8
+ export type GithubCliResult = {
9
+ available: boolean;
10
+ reason?: string;
11
+ nextAction?: string;
12
+ };
13
+ export type GithubSubmitOutcome = {
14
+ ok: true;
15
+ issueUrl: string;
16
+ } | {
17
+ ok: false;
18
+ reason: string;
19
+ nextAction: string;
20
+ };
21
+ export declare const DEFAULT_GH_TIMEOUT_MS = 30000;
22
+ /**
23
+ * Detect whether the gh channel is usable: gh exists and `gh auth status` exits 0.
24
+ * Any execFile error (command missing / not authenticated / other) reports
25
+ * unavailable with a structured nextAction. Fail-closed here — unlike channel
26
+ * probing, a non-zero auth status genuinely means submission would fail.
27
+ */
28
+ export declare function detectGithubCli(deps?: ChannelDeps): Promise<GithubCliResult>;
29
+ /**
30
+ * gh issue create --repo <repo> --title <title> --body-file <tmp> --label feedback
31
+ * Writes the markdown body to a 0600 temp file, runs gh, deletes the temp file,
32
+ * then parses the issue URL from gh's stdout.
33
+ */
34
+ export declare function submitToGithub(args: {
35
+ repo: string;
36
+ proxy?: string;
37
+ report: FeedbackReport;
38
+ execFile?: ExecFileFn;
39
+ fs?: typeof fsNS;
40
+ os?: typeof osNS;
41
+ path?: typeof pathNS;
42
+ timeoutMs?: number;
43
+ }): Promise<GithubSubmitOutcome>;
44
+ export {};
@@ -0,0 +1,125 @@
1
+ // github-adapter.ts
2
+ // Server-side gh CLI channel adapter (spec §8.4). Secondary channel for
3
+ // developers who can reach GitHub (proxy may be required domestically).
4
+ //
5
+ // - `execFile` with an argument array — zero shell concatenation (injection-safe).
6
+ // - Body written to a 0600 temp file, deleted after submission.
7
+ // - `github_proxy` sets HTTPS_PROXY for the child process when non-empty.
8
+ // - 30s default timeout; success parses the issue URL from stdout.
9
+ export const DEFAULT_GH_TIMEOUT_MS = 30_000;
10
+ const GH_COMMAND = 'gh';
11
+ function execFileAsync(execFile, opts) {
12
+ return new Promise((resolve, reject) => {
13
+ execFile(opts.cmd, opts.args, {
14
+ timeout: opts.timeoutMs,
15
+ windowsHide: true,
16
+ env: opts.proxy
17
+ ? { ...process.env, HTTPS_PROXY: opts.proxy, https_proxy: opts.proxy }
18
+ : process.env,
19
+ }, (err, stdout, stderr) => {
20
+ if (err) {
21
+ reject(err);
22
+ return;
23
+ }
24
+ resolve({ code: 0, stdout: stdout ?? '', stderr: stderr ?? '' });
25
+ });
26
+ });
27
+ }
28
+ async function safeUnlink(fsMod, file) {
29
+ try {
30
+ await fsMod.promises.unlink(file);
31
+ }
32
+ catch {
33
+ // Best-effort cleanup; the tmp dir is system-managed.
34
+ }
35
+ }
36
+ /**
37
+ * Detect whether the gh channel is usable: gh exists and `gh auth status` exits 0.
38
+ * Any execFile error (command missing / not authenticated / other) reports
39
+ * unavailable with a structured nextAction. Fail-closed here — unlike channel
40
+ * probing, a non-zero auth status genuinely means submission would fail.
41
+ */
42
+ export async function detectGithubCli(deps) {
43
+ const execFile = deps?.execFile ?? (await import('node:child_process')).execFile;
44
+ // 1. gh present? A success here means the version probe ran without error.
45
+ try {
46
+ await execFileAsync(execFile, { cmd: GH_COMMAND, args: ['--version'], timeoutMs: 5_000 });
47
+ }
48
+ catch {
49
+ return {
50
+ available: false,
51
+ reason: '未安装 gh CLI',
52
+ nextAction: '安装 GitHub CLI:https://cli.github.com/ 后重试',
53
+ };
54
+ }
55
+ // 2. authenticated?
56
+ try {
57
+ await execFileAsync(execFile, { cmd: GH_COMMAND, args: ['auth', 'status'], timeoutMs: 5_000 });
58
+ return { available: true };
59
+ }
60
+ catch {
61
+ return {
62
+ available: false,
63
+ reason: 'gh 未登录 GitHub',
64
+ nextAction: '运行 `gh auth login` 完成登录后重试',
65
+ };
66
+ }
67
+ }
68
+ /**
69
+ * gh issue create --repo <repo> --title <title> --body-file <tmp> --label feedback
70
+ * Writes the markdown body to a 0600 temp file, runs gh, deletes the temp file,
71
+ * then parses the issue URL from gh's stdout.
72
+ */
73
+ export async function submitToGithub(args) {
74
+ const execFile = args.execFile ?? (await import('node:child_process')).execFile;
75
+ const fsMod = args.fs ?? (await import('node:fs'));
76
+ const osMod = args.os ?? (await import('node:os'));
77
+ const pathMod = args.path ?? (await import('node:path'));
78
+ const timeoutMs = args.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS;
79
+ const { title, id, outputs } = args.report;
80
+ const body = outputs?.markdown ?? '';
81
+ // Write body to a 0600 temp file to avoid leaking via argv / shell quoting.
82
+ const tmpFile = pathMod.join(osMod.tmpdir(), `pd-feedback-${id}.md`);
83
+ try {
84
+ await fsMod.promises.writeFile(tmpFile, body, { encoding: 'utf8', mode: 0o600 });
85
+ }
86
+ catch (err) {
87
+ return {
88
+ ok: false,
89
+ reason: `无法写入临时文件写入 gh 正文:${err instanceof Error ? err.message : String(err)}`,
90
+ nextAction: '检查临时目录可写后重试;或改用反馈服务通道',
91
+ };
92
+ }
93
+ let out;
94
+ try {
95
+ out = await execFileAsync(execFile, {
96
+ cmd: GH_COMMAND,
97
+ args: ['issue', 'create', '--repo', args.repo, '--title', title, '--body-file', tmpFile, '--label', 'feedback'],
98
+ timeoutMs,
99
+ proxy: args.proxy && args.proxy.length > 0 ? args.proxy : undefined,
100
+ });
101
+ }
102
+ catch (err) {
103
+ await safeUnlink(fsMod, tmpFile);
104
+ const reason = err instanceof Error
105
+ ? err.code === 'ETIMEDOUT'
106
+ ? `gh 提交超时(${timeoutMs}ms)`
107
+ : err.message
108
+ : String(err);
109
+ return {
110
+ ok: false,
111
+ reason: `gh 建单失败:${reason}`,
112
+ nextAction: '确认 gh 已登录且网络可达(可能需要代理);重试或改用反馈服务通道',
113
+ };
114
+ }
115
+ await safeUnlink(fsMod, tmpFile);
116
+ const urlMatch = /\bhttps:\/\/github\.com\/\S+\b/.exec(out.stdout);
117
+ if (!urlMatch) {
118
+ return {
119
+ ok: false,
120
+ reason: 'gh 建单成功但无法解析 issue 链接',
121
+ nextAction: '在 GitHub 仓库确认新 issue 已创建,并把链接手动记下',
122
+ };
123
+ }
124
+ return { ok: true, issueUrl: urlMatch[0] };
125
+ }
@@ -0,0 +1,45 @@
1
+ import type { FeedbackReport } from '@principles/core/runtime-v2/feedback';
2
+ export interface IngestHealthResult {
3
+ available: boolean;
4
+ reason?: string;
5
+ nextAction?: string;
6
+ }
7
+ export type IngestSubmitOutcome = {
8
+ ok: true;
9
+ trackingId?: string;
10
+ issueUrl?: string;
11
+ duplicate: boolean;
12
+ count?: number;
13
+ } | {
14
+ ok: false;
15
+ /** HTTP status from the relay when reachable; 0 for network/timeout failure. */
16
+ status: number;
17
+ reason: string;
18
+ nextAction: string;
19
+ };
20
+ export declare const DEFAULT_INGEST_TIMEOUT_MS = 20000;
21
+ export declare const DEFAULT_PROBE_TIMEOUT_MS = 2500;
22
+ /**
23
+ * Probe the relay `/health` endpoint (spec §9.1) for channels availability.
24
+ * Any HTTP response (2xx/4xx/5xx) other than a network/timeout error counts as
25
+ * "reachable" (fail-open — an auth'd health endpoint may legitimately 401; we
26
+ * only need to confirm a server exists). Non-2xx → available:true so the submit
27
+ * path can surface a precise relay reason instead of hiding the channel.
28
+ */
29
+ export declare function probeIngestHealth(ingestUrl: string, deps?: {
30
+ fetchFn?: typeof fetch;
31
+ timeoutMs?: number;
32
+ }): Promise<IngestHealthResult>;
33
+ /**
34
+ * Submit a report body to the relay (POST {ingestUrl}).
35
+ * Success = any 2xx; the relay returns 202 with { trackingId, issueUrl, duplicate, count }.
36
+ */
37
+ export declare function submitToIngest(args: {
38
+ url: string;
39
+ token: string;
40
+ report: FeedbackReport;
41
+ fingerprint: string;
42
+ area?: string;
43
+ fetchFn?: typeof fetch;
44
+ timeoutMs?: number;
45
+ }): Promise<IngestSubmitOutcome>;
@@ -0,0 +1,129 @@
1
+ // ingest-adapter.ts
2
+ // Server-side adapter that POSTs a redacted, saved feedback draft to the
3
+ // configured relay endpoint (spec §8.3). The relay is reachable domestically
4
+ // (same origin as the official site), so domestic novices never touch GitHub.
5
+ //
6
+ // - Token lives only in server config (never the browser).
7
+ // - 20s default timeout (relay calls Linear on the happy path).
8
+ // - Failure returns a structured reason + nextAction (rc-9).
9
+ export const DEFAULT_INGEST_TIMEOUT_MS = 20_000;
10
+ export const DEFAULT_PROBE_TIMEOUT_MS = 2_500;
11
+ async function fetchWithTimeout(args) {
12
+ const { fetchFn, url, init, timeoutMs } = args;
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15
+ try {
16
+ return await fetchFn(url, { ...init, signal: controller.signal });
17
+ }
18
+ finally {
19
+ clearTimeout(timer);
20
+ }
21
+ }
22
+ /**
23
+ * Probe the relay `/health` endpoint (spec §9.1) for channels availability.
24
+ * Any HTTP response (2xx/4xx/5xx) other than a network/timeout error counts as
25
+ * "reachable" (fail-open — an auth'd health endpoint may legitimately 401; we
26
+ * only need to confirm a server exists). Non-2xx → available:true so the submit
27
+ * path can surface a precise relay reason instead of hiding the channel.
28
+ */
29
+ export async function probeIngestHealth(ingestUrl, deps) {
30
+ const fetchFn = deps?.fetchFn ?? globalThis.fetch;
31
+ const timeoutMs = deps?.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
32
+ const healthUrl = `${ingestUrl.replace(/\/+$/, '')}/health`;
33
+ if (typeof fetchFn !== 'function') {
34
+ return {
35
+ available: false,
36
+ reason: '当前运行环境不支持 fetch',
37
+ nextAction: '使用 Node 18+ 运行 console,或改用导出文件通道',
38
+ };
39
+ }
40
+ try {
41
+ await fetchWithTimeout({ fetchFn, url: healthUrl, init: { method: 'GET' }, timeoutMs });
42
+ return { available: true };
43
+ }
44
+ catch (err) {
45
+ const reason = err instanceof Error
46
+ ? (err.name === 'AbortError' ? `探活超时(${timeoutMs}ms)` : err.message)
47
+ : String(err);
48
+ return {
49
+ available: false,
50
+ reason: `无法连接反馈服务:${reason}`,
51
+ nextAction: '检查网络;或改用导出文件通道',
52
+ };
53
+ }
54
+ }
55
+ /**
56
+ * Submit a report body to the relay (POST {ingestUrl}).
57
+ * Success = any 2xx; the relay returns 202 with { trackingId, issueUrl, duplicate, count }.
58
+ */
59
+ export async function submitToIngest(args) {
60
+ const { url, token } = args;
61
+ const fetchFn = args.fetchFn ?? globalThis.fetch;
62
+ const timeoutMs = args.timeoutMs ?? DEFAULT_INGEST_TIMEOUT_MS;
63
+ const payload = {
64
+ report: args.report,
65
+ fingerprint: args.fingerprint,
66
+ area: typeof args.area === 'string' ? args.area : undefined,
67
+ };
68
+ let res;
69
+ try {
70
+ res = await fetchWithTimeout({
71
+ fetchFn,
72
+ url,
73
+ init: {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ Authorization: `Bearer ${token}`,
78
+ },
79
+ body: JSON.stringify(payload),
80
+ },
81
+ timeoutMs,
82
+ });
83
+ }
84
+ catch (err) {
85
+ const reason = err instanceof Error
86
+ ? (err.name === 'AbortError' ? `提交超时(${timeoutMs}ms)` : err.message)
87
+ : String(err);
88
+ return {
89
+ ok: false,
90
+ status: 0,
91
+ reason: `反馈服务不可达:${reason}`,
92
+ nextAction: '检查网络后重试;或使用导出文件通道发送给维护者',
93
+ };
94
+ }
95
+ if (res.status >= 200 && res.status < 300) {
96
+ const outcome = {
97
+ ok: true,
98
+ duplicate: false,
99
+ };
100
+ try {
101
+ const body = await res.json();
102
+ if (body !== null && typeof body === 'object') {
103
+ const b = body;
104
+ if (typeof b.trackingId === 'string')
105
+ outcome.trackingId = b.trackingId;
106
+ if (typeof b.issueUrl === 'string')
107
+ outcome.issueUrl = b.issueUrl;
108
+ if (typeof b.duplicate === 'boolean')
109
+ outcome.duplicate = b.duplicate;
110
+ if (typeof b.count === 'number')
111
+ outcome.count = b.count;
112
+ }
113
+ }
114
+ catch {
115
+ // 2xx with non-JSON body: still consider submission accepted; no receipt fields.
116
+ }
117
+ return outcome;
118
+ }
119
+ return {
120
+ ok: false,
121
+ status: res.status,
122
+ reason: `反馈服务返回 ${res.status}`,
123
+ nextAction: res.status === 429
124
+ ? '请求过于频繁,请稍后重试'
125
+ : res.status === 413
126
+ ? '内容过长,请精简后重试'
127
+ : `反馈服务异常,请稍后重试;或导出文件发送给维护者`,
128
+ };
129
+ }
@@ -0,0 +1,44 @@
1
+ import { type FeedbackSubmittedVia } from '@principles/core/runtime-v2/feedback';
2
+ import type * as childProcessNS from 'node:child_process';
3
+ import type * as fsNS from 'node:fs';
4
+ import type * as osNS from 'node:os';
5
+ import type * as pathNS from 'node:path';
6
+ import type { FeedbackChannelConfig } from '../config/pd-config-store.js';
7
+ import type { FeedbackReportConsoleModel } from '../models/FeedbackReportConsoleModel.js';
8
+ type ExecFileFn = typeof childProcessNS.execFile;
9
+ export type SubmitChannel = 'ingest' | 'github';
10
+ export type SubmitReportResult = {
11
+ ok: true;
12
+ alreadySubmitted: boolean;
13
+ status: 'submitted';
14
+ submittedVia?: FeedbackSubmittedVia;
15
+ trackingId?: string;
16
+ externalUrl?: string;
17
+ writeBackFailed?: boolean;
18
+ nextAction?: string;
19
+ } | {
20
+ ok: false;
21
+ statusCode: number;
22
+ error: string;
23
+ message: string;
24
+ nextAction: string;
25
+ };
26
+ export type SubmitDeps = {
27
+ fetchFn?: typeof fetch;
28
+ execFile?: ExecFileFn;
29
+ fs?: typeof fsNS;
30
+ os?: typeof osNS;
31
+ path?: typeof pathNS;
32
+ };
33
+ /**
34
+ * Submit a saved draft by id through the given channel. `maintainerEmail`
35
+ * (route-level) is accepted separately for parity/future use.
36
+ */
37
+ export declare function submitReport(opts: {
38
+ model: FeedbackReportConsoleModel;
39
+ reportId: string;
40
+ channel: SubmitChannel;
41
+ config: FeedbackChannelConfig;
42
+ deps?: SubmitDeps;
43
+ }): Promise<SubmitReportResult>;
44
+ export {};