@x-otto/plugin-weather 0.1.0-alpha.5 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Provides a single `get_weather` tool that fetches current weather and a 3-day fo
8
8
 
9
9
  This plugin contributes two things:
10
10
 
11
- 1. **Tool** (`get_weather`) — Geocodes a city name, fetches the 7-day forecast from Open-Meteo, and returns structured data with current conditions + 3-day summary.
11
+ 1. **Tool** (`get_weather`) — Geocodes a city name, fetches the 3-day forecast from Open-Meteo, and returns structured data with current conditions + 3-day summary.
12
12
  2. **Renderer** — A terminal card renderer (`matcher.toolName: "get_weather"`) that formats the tool result as a pretty TUI card with temperature, description, and forecast details.
13
13
 
14
14
  The tool returns both a plain-text summary (for the conversation stream) and structured `details` (for the renderer to consume without string parsing).
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@x-otto/plugin-weather",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.6",
4
4
  "private": false,
5
5
  "description": "Weather forecast plugin — get_weather tool (Open-Meteo, no API key) + terminal card renderer",
6
6
  "devDependencies": {
7
- "@x-otto/interchange": "0.1.0-alpha.4"
7
+ "@x-otto/interchange": "0.1.0-alpha.8"
8
8
  },
9
9
  "type": "module",
10
10
  "files": [
@@ -12,6 +12,8 @@
12
12
  "README.md",
13
13
  "plugin.ts",
14
14
  "src",
15
+ "renderers",
16
+ "renderers-dist",
15
17
  "plugin-dist"
16
18
  ],
17
19
  "publishConfig": {
@@ -20,7 +22,7 @@
20
22
  "tag": "alpha"
21
23
  },
22
24
  "dependencies": {
23
- "@x-otto/plugin": "0.1.0-alpha.5"
25
+ "@x-otto/plugin": "0.1.0-alpha.8"
24
26
  },
25
27
  "scripts": {
26
28
  "typecheck": "tsc --noEmit",
@@ -1 +1 @@
1
- {"apiVersion":1,"compilerVersion":"0.1.0-alpha.6","sourceHash":"bed185533d663b69a36b881d8540349deb90b100fa4b51dc2adafa0b8baec44a"}
1
+ {"apiVersion":1,"compilerVersion":"0.1.0-alpha.8","sourceHash":"77c57e232f4a06a5f27a09c936273f194f49cbbd4655c0e53eeca6a79c6c2748"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * weather.ts —— `get_weather` 工具结果的终端卡片渲染器(RFC-105 D6 渲染器型)。
3
+ *
4
+ * 签名:`(message: unknown, columns: number) => string[]`,与 `renderMessageLines` 同构
5
+ * (`@x-otto/tui` 消息级渲染的原生货币是 ANSI 字符串数组,见 `plugin-renderer-registry.ts`)。
6
+ * `message` 是完整 `ToolResultMessage`,`details` 字段携带 `plugin.ts` 产出的结构化
7
+ * `WeatherData`(同 `src/types.ts` 定义,两侧手动保持字段一致,编译产物各自独立无法共享类型)。
8
+ *
9
+ * fail-soft:`details` 缺失/形状不符时返回空数组(宿主 `tryRenderWithPlugins` 视为
10
+ * "未接管",回退默认文本渲染,见 R6),不抛异常。
11
+ */
12
+
13
+ /**
14
+ * ANSI 24-bit 色常量 —— 插件独立产物无法 import @x-otto/tui THEME 常量,此处硬编码
15
+ * 与 TUI 设计语言语义角色对齐的值:
16
+ * - panelBorder / inactive = #9498B8(注释蓝灰,TUI 统一面板边框色与次级文本色)
17
+ * - warning = #E5C07B(One-Dark 语法黄色,用于极端天气提示)
18
+ *
19
+ * 配色只出现一次(这里),后续渲染全部引用常量名而非裸值。
20
+ *
21
+ * RFC-303 M3(T303-09):三个常量此前均是空 CSI 前缀(`${CSI}` 缺少真正的 SGR 参数
22
+ * 与结尾 `m`),拼接后产出畸形转义序列——终端不解析为颜色,而是原样打印或静默吞掉,
23
+ * 8 处消费点全部无色输出。旧版测试(`renderer.test.ts`)只断言 `toContain('\x1b[')`,
24
+ * 对这个具体值恒真,是本仓已知的恒绿测试盲区(经历过两轮 review 未被发现)。已按上述
25
+ * 声明的语义色值补齐 24-bit ANSI 前景色(`\x1b[38;2;R;G;Bm`):panelBorder/inactive
26
+ * = rgb(148,152,184),warning = rgb(229,192,123)。
27
+ */
28
+ const CSI = '\x1b['
29
+ const RESET = `${CSI}0m`
30
+ const ANSI_BORDER = `${CSI}38;2;148;152;184m`
31
+ const ANSI_INACTIVE = `${CSI}38;2;148;152;184m`
32
+ const ANSI_WARNING = `${CSI}38;2;229;192;123m`
33
+
34
+ /** 单位常量(终局 review 补齐,避免裸字面量散布多处)。 */
35
+ const TEMP_UNIT = '°C'
36
+ const WIND_SPEED_UNIT = 'km/h'
37
+
38
+ /** 检测极端天气描述,命中时渲染行使用 warning 色。 */
39
+ function isSevereWeather(desc: string): boolean {
40
+ return /thunderstorm|heavy|violent/i.test(desc)
41
+ }
42
+
43
+ interface WeatherRenderData {
44
+ city: string
45
+ country?: string
46
+ current: {
47
+ temperature: number
48
+ humidity: number
49
+ windSpeed: number
50
+ description: string
51
+ icon: string
52
+ }
53
+ forecast: Array<{
54
+ date: string
55
+ tempMax: number
56
+ tempMin: number
57
+ description: string
58
+ icon: string
59
+ }>
60
+ }
61
+
62
+ const CARD_WIDTH_MIN = 30
63
+
64
+ function isWeatherData(value: unknown): value is WeatherRenderData {
65
+ if (!value || typeof value !== 'object') return false
66
+ const v = value as Record<string, unknown>
67
+ return typeof v.city === 'string' && v.city.length > 0 && typeof v.current === 'object' && Array.isArray(v.forecast)
68
+ }
69
+
70
+ /**
71
+ * 简化版终端显示宽度估算(emoji/CJK 等宽字符按 2 列计,其余按 1 列计)。不追求
72
+ * Unicode 规范级精确(如变体选择符/ZWJ 序列的精确 grapheme 聚类),够本卡片对齐场景使用——
73
+ * 独立轻量插件不引入 `string-width`(纯 ESM + 完整 Unicode 数据表,对一个渲染函数是重依赖)。
74
+ */
75
+ function displayWidth(text: string): number {
76
+ let width = 0
77
+ for (const ch of text) {
78
+ const code = ch.codePointAt(0) ?? 0
79
+ // 变体选择符(U+FE0F 等)零宽——不单独占列,只是修饰前一个字符的呈现形式
80
+ // (如 ☀️ = U+2600 + U+FE0F,作为两个独立 code point 被 for...of 拆开)。
81
+ if (code === 0xfe0f || code === 0xfe0e) continue
82
+ // CJK 统一表意文字/标点、韩文音节、日文假名,以及 emoji 常见区段(misc symbols/
83
+ // dingbats/emoticons/transport/supplemental symbols)均按全角(2列)计。
84
+ const isWide =
85
+ (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
86
+ (code >= 0x2e80 && code <= 0xa4cf) || // CJK Radicals ~ Yi
87
+ (code >= 0xac00 && code <= 0xd7a3) || // Hangul Syllables
88
+ (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
89
+ (code >= 0xff00 && code <= 0xff60) || // Fullwidth Forms
90
+ (code >= 0x2600 && code <= 0x27bf) || // Misc symbols / Dingbats(含 ☀️☁️⛅ 等)
91
+ (code >= 0x1f300 && code <= 0x1fadf) // emoji 主区段
92
+ width += isWide ? 2 : 1
93
+ }
94
+ return width
95
+ }
96
+
97
+ /** 补齐到目标显示宽度;超长则截断(按 code point 逐步剪短直到显示宽度不超)。 */
98
+ function pad(text: string, width: number): string {
99
+ let w = displayWidth(text)
100
+ if (w <= width) return text + ' '.repeat(width - w)
101
+ let out = text
102
+ while (w > width && out.length > 0) {
103
+ out = [...out].slice(0, -1).join('')
104
+ w = displayWidth(out)
105
+ }
106
+ return out
107
+ }
108
+
109
+ export default function renderWeatherCard(message: unknown, columns: number): string[] {
110
+ const m = message as Record<string, unknown>
111
+ const details = m.details
112
+ if (!isWeatherData(details)) return []
113
+
114
+ // Nav 风格无竖线,宽度 = 内容宽度(不需扣 2 列边框裕量)
115
+ const width = Math.max(CARD_WIDTH_MIN, Math.min(columns, 40))
116
+
117
+ const location = details.country ? `${details.city}, ${details.country}` : details.city
118
+ const out: string[] = []
119
+
120
+ // 顶部横线 — nav 风格(仅上下单线,无左/右竖线),panelBorder 色
121
+ out.push(`${ANSI_BORDER}${'─'.repeat(width)}${RESET}`)
122
+
123
+ // 城市/国家(默认终端色)
124
+ out.push(pad(location, width))
125
+
126
+ // 温度 + 描述(默认终端色;极端天气整行 warning 色)
127
+ const currentLine = `${details.current.icon} ${details.current.temperature}${TEMP_UNIT} ${details.current.description}`
128
+ if (isSevereWeather(details.current.description)) {
129
+ out.push(`${ANSI_WARNING}${pad(currentLine, width)}${RESET}`)
130
+ } else {
131
+ out.push(pad(currentLine, width))
132
+ }
133
+
134
+ // 湿度 + 风速 — 次级色(THEME.inactive = #9498B8)
135
+ const weatherLine = `💧 ${details.current.humidity}% 💨 ${details.current.windSpeed} ${WIND_SPEED_UNIT}`
136
+ out.push(`${ANSI_INACTIVE}${pad(weatherLine, width)}${RESET}`)
137
+
138
+ // 分隔横线 — nav 风格
139
+ out.push(`${ANSI_BORDER}${'─'.repeat(width)}${RESET}`)
140
+
141
+ // 预报行(极端天气行 warning 色)
142
+ for (const day of details.forecast) {
143
+ const line = `${day.date} ${day.icon} ${day.tempMin}° / ${day.tempMax}° ${day.description}`
144
+ if (isSevereWeather(day.description)) {
145
+ out.push(`${ANSI_WARNING}${pad(line, width)}${RESET}`)
146
+ } else {
147
+ out.push(pad(line, width))
148
+ }
149
+ }
150
+
151
+ // 底部横线
152
+ out.push(`${ANSI_BORDER}${'─'.repeat(width)}${RESET}`)
153
+
154
+ return out
155
+ }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // extensions/plugin-weather/renderers/weather.ts
21
+ var weather_exports = {};
22
+ __export(weather_exports, {
23
+ default: () => renderWeatherCard
24
+ });
25
+ module.exports = __toCommonJS(weather_exports);
26
+ var CSI = "\x1B[";
27
+ var RESET = `${CSI}0m`;
28
+ var ANSI_BORDER = `${CSI}38;2;148;152;184m`;
29
+ var ANSI_INACTIVE = `${CSI}38;2;148;152;184m`;
30
+ var ANSI_WARNING = `${CSI}38;2;229;192;123m`;
31
+ var TEMP_UNIT = "\xB0C";
32
+ var WIND_SPEED_UNIT = "km/h";
33
+ function isSevereWeather(desc) {
34
+ return /thunderstorm|heavy|violent/i.test(desc);
35
+ }
36
+ var CARD_WIDTH_MIN = 30;
37
+ function isWeatherData(value) {
38
+ if (!value || typeof value !== "object") return false;
39
+ const v = value;
40
+ return typeof v.city === "string" && v.city.length > 0 && typeof v.current === "object" && Array.isArray(v.forecast);
41
+ }
42
+ function displayWidth(text) {
43
+ let width = 0;
44
+ for (const ch of text) {
45
+ const code = ch.codePointAt(0) ?? 0;
46
+ if (code === 65039 || code === 65038) continue;
47
+ const isWide = code >= 4352 && code <= 4447 || // Hangul Jamo
48
+ code >= 11904 && code <= 42191 || // CJK Radicals ~ Yi
49
+ code >= 44032 && code <= 55203 || // Hangul Syllables
50
+ code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
51
+ code >= 65280 && code <= 65376 || // Fullwidth Forms
52
+ code >= 9728 && code <= 10175 || // Misc symbols / Dingbats(含 ☀️☁️⛅ 等)
53
+ code >= 127744 && code <= 129759;
54
+ width += isWide ? 2 : 1;
55
+ }
56
+ return width;
57
+ }
58
+ function pad(text, width) {
59
+ let w = displayWidth(text);
60
+ if (w <= width) return text + " ".repeat(width - w);
61
+ let out = text;
62
+ while (w > width && out.length > 0) {
63
+ out = [...out].slice(0, -1).join("");
64
+ w = displayWidth(out);
65
+ }
66
+ return out;
67
+ }
68
+ function renderWeatherCard(message, columns) {
69
+ const m = message;
70
+ const details = m.details;
71
+ if (!isWeatherData(details)) return [];
72
+ const width = Math.max(CARD_WIDTH_MIN, Math.min(columns, 40));
73
+ const location = details.country ? `${details.city}, ${details.country}` : details.city;
74
+ const out = [];
75
+ out.push(`${ANSI_BORDER}${"\u2500".repeat(width)}${RESET}`);
76
+ out.push(pad(location, width));
77
+ const currentLine = `${details.current.icon} ${details.current.temperature}${TEMP_UNIT} ${details.current.description}`;
78
+ if (isSevereWeather(details.current.description)) {
79
+ out.push(`${ANSI_WARNING}${pad(currentLine, width)}${RESET}`);
80
+ } else {
81
+ out.push(pad(currentLine, width));
82
+ }
83
+ const weatherLine = `\u{1F4A7} ${details.current.humidity}% \u{1F4A8} ${details.current.windSpeed} ${WIND_SPEED_UNIT}`;
84
+ out.push(`${ANSI_INACTIVE}${pad(weatherLine, width)}${RESET}`);
85
+ out.push(`${ANSI_BORDER}${"\u2500".repeat(width)}${RESET}`);
86
+ for (const day of details.forecast) {
87
+ const line = `${day.date} ${day.icon} ${day.tempMin}\xB0 / ${day.tempMax}\xB0 ${day.description}`;
88
+ if (isSevereWeather(day.description)) {
89
+ out.push(`${ANSI_WARNING}${pad(line, width)}${RESET}`);
90
+ } else {
91
+ out.push(pad(line, width));
92
+ }
93
+ }
94
+ out.push(`${ANSI_BORDER}${"\u2500".repeat(width)}${RESET}`);
95
+ return out;
96
+ }