File size: 5,920 Bytes
96e86e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import os from "node:os";
import {
  DEFAULT_UI_LOCALE,
  type UiLocale,
} from "@penclipai/shared";

type ResolveRuntimeLocalizationPromptInput = {
  locale: UiLocale;
  platform?: NodeJS.Platform;
  shell?: string | null;
  env?: NodeJS.ProcessEnv;
  osRelease?: string | null;
};

type RuntimeEnvironmentDescriptor = {
  labelZh: string;
  labelEn: string;
};

function readNonEmptyString(value: unknown): string | null {
  return typeof value === "string" && value.trim().length > 0 ? value : null;
}

function parseSupportedUiLocale(value: unknown): UiLocale | null {
  const candidate = readNonEmptyString(value);
  if (!candidate) return null;
  const normalized = candidate.trim().toLowerCase();
  if (normalized.startsWith("zh")) return "zh-CN";
  if (normalized.startsWith("en")) return "en";
  return null;
}

function stripExecutableName(value: string | null | undefined): string | null {
  const trimmed = value?.trim();
  if (!trimmed) return null;
  const parts = trimmed.split(/[\\/]/g);
  return parts[parts.length - 1] || trimmed;
}

function isPowerShellShell(shell: string | null, env: NodeJS.ProcessEnv): boolean {
  if (shell && /(^|[\\/])(pwsh|powershell)(\.exe)?$/i.test(shell)) {
    return true;
  }
  return Boolean(env.POWERSHELL_DISTRIBUTION_CHANNEL || env.PSExecutionPolicyPreference);
}

function isCmdShell(shell: string | null): boolean {
  return Boolean(shell && /(^|[\\/])cmd(\.exe)?$/i.test(shell));
}

function isWslRuntime(platform: NodeJS.Platform, env: NodeJS.ProcessEnv, osRelease: string | null): boolean {
  if (platform !== "linux") return false;
  return Boolean(
    env.WSL_DISTRO_NAME
    || env.WSL_INTEROP
    || (osRelease && /microsoft/i.test(osRelease)),
  );
}

function resolveRuntimeEnvironment(
  input: ResolveRuntimeLocalizationPromptInput,
): RuntimeEnvironmentDescriptor {
  const platform = input.platform ?? process.platform;
  const env = input.env ?? process.env;
  const shell = input.shell?.trim()
    || env.SHELL?.trim()
    || env.ComSpec?.trim()
    || env.COMSPEC?.trim()
    || null;
  const shellName = stripExecutableName(shell);
  const osRelease = input.osRelease ?? (platform === "linux" ? os.release() : null);

  if (isWslRuntime(platform, env, osRelease)) {
    const wslShell = shellName ?? "sh";
    return {
      labelZh: `WSL ${wslShell}`,
      labelEn: `WSL ${wslShell}`,
    };
  }

  if (platform === "win32") {
    if (isPowerShellShell(shell, env)) {
      return {
        labelZh: "Windows PowerShell",
        labelEn: "Windows PowerShell",
      };
    }

    if (isCmdShell(shell)) {
      return {
        labelZh: "Windows cmd.exe",
        labelEn: "Windows cmd.exe",
      };
    }

    if (shellName) {
      return {
        labelZh: `Windows shell (${shellName})`,
        labelEn: `Windows shell (${shellName})`,
      };
    }

    return {
      labelZh: "Windows",
      labelEn: "Windows",
    };
  }

  if (shellName) {
    return {
      labelZh: `${shellName} on ${platform}`,
      labelEn: `${shellName} on ${platform}`,
    };
  }

  return {
    labelZh: platform,
    labelEn: platform,
  };
}

function buildZhCnRuntimeLocalizationPrompt(environment: RuntimeEnvironmentDescriptor): string {
  return [
    "## 语言与运行时契约",
    "- 输出契约:除非用户本轮明确要求其他语言,所有面向用户的自然语言输出必须使用简体中文;代码、命令、路径、API 字段名和日志原文保持原样。",
    `- 宿主环境:${environment.labelZh}。`,
    "- CLI 契约:执行 Paperclip 命令一律使用 `penclip ...`;仅在逐字引用用户文本、日志或历史文档时保留 `paperclipai ...`。",
    "- API 契约:优先使用 `penclip` CLI 完成 Paperclip 操作;只有在 CLI 无法覆盖时,才直接调用 API,并确保带上必需的认证与运行标头。",
  ].join("\n");
}

function buildEnRuntimeLocalizationPrompt(environment: RuntimeEnvironmentDescriptor): string {
  return [
    "## Language and Runtime Contract",
    "- Output contract: unless the current user request explicitly asks for another language, all user-facing natural-language output must be in English. Keep code, commands, file paths, API field names, and raw logs verbatim.",
    `- Host runtime: ${environment.labelEn}.`,
    "- CLI contract: use `penclip ...` for Paperclip commands; keep `paperclipai ...` only in verbatim quotes from user text, logs, or historical docs.",
    "- API contract: prefer the `penclip` CLI for Paperclip operations; only call the HTTP API directly when the CLI cannot cover the action, and always include the required auth and run headers.",
  ].join("\n");
}

export function readRuntimeUiLocaleFromContextSnapshot(
  contextSnapshot: Record<string, unknown> | null | undefined,
): UiLocale | null {
  return parseSupportedUiLocale(contextSnapshot?.runtimeUiLocale);
}

export function resolveEffectiveRuntimeUiLocale(input: {
  requestedUiLocale?: unknown;
  runtimeUiLocale?: unknown;
  runtimeDefaultLocale?: unknown;
}): UiLocale {
  return (
    parseSupportedUiLocale(input.requestedUiLocale) ??
    parseSupportedUiLocale(input.runtimeUiLocale) ??
    parseSupportedUiLocale(input.runtimeDefaultLocale) ??
    DEFAULT_UI_LOCALE
  );
}

export function resolveEffectiveRuntimeUiLocaleForContextSnapshot(
  contextSnapshot: Record<string, unknown> | null | undefined,
  runtimeDefaultLocale?: unknown,
): UiLocale {
  return resolveEffectiveRuntimeUiLocale({
    requestedUiLocale: contextSnapshot?.requestedUiLocale,
    runtimeUiLocale: contextSnapshot?.runtimeUiLocale,
    runtimeDefaultLocale,
  });
}

export function resolveRuntimeLocalizationPrompt(
  input: ResolveRuntimeLocalizationPromptInput,
): string {
  const environment = resolveRuntimeEnvironment(input);
  return input.locale === "en"
    ? buildEnRuntimeLocalizationPrompt(environment)
    : buildZhCnRuntimeLocalizationPrompt(environment);
}