Skip to content

Commit a6559dc

Browse files
cifangyiquanguanchen.lin
andauthored
sync: builtin catalog 20 → 25 tools (#2)
Regenerate scaffold from private main (0be1055) and bring the public catalog in sync. Adds 5 builtins: - qrcode-generate (QR code → SVG, byte mode ECC-L, v1-v10, pure JS) - image-data-url-inspect - youtube-url-normalize / youtube-oembed-read / youtube-info-format source digest sha256:6ebd1d8e… → sha256:54efa168… scaffold digest sha256:97c6d108… → sha256:e2b2980b… Co-authored-by: guanchen.lin <guanchen.lin@users.noreply.github.com>
1 parent e1aeb68 commit a6559dc

2 files changed

Lines changed: 299 additions & 4 deletions

File tree

.developer-preview-scaffold.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schemaVersion": "toolkit.developer-preview-public-repo-scaffold.v1",
33
"packageName": "toolkit-builtins",
4-
"generatedAt": "2026-08-08T13:57:50.290Z",
4+
"generatedAt": "2026-09-03T13:28:10.703Z",
55
"boundaryVersion": "toolkit.community-package-boundary.v1",
66
"cleanSplitMode": "clean-directory-no-private-history",
77
"registryTrust": "disabled-for-developer-preview",
@@ -16,8 +16,8 @@
1616
"sourceFiles": [
1717
{
1818
"path": "scripts/builtin_tools.js",
19-
"byteLength": 30201,
20-
"digest": "sha256:6ebd1d8eeaca129f81379f8fb8a89299ecf3068c84444c7d8c799f276eab1237",
19+
"byteLength": 44868,
20+
"digest": "sha256:54efa168e99546d10eefe9818ddf5a46f7a3d2a90d8628ccc02f5ab450da6e11",
2121
"generated": false
2222
},
2323
{
@@ -95,5 +95,5 @@
9595
".github/workflows/release.yml"
9696
],
9797
"protocolConformanceBundleDigest": "",
98-
"digest": "sha256:97c6d108af51507ee97035c7e9110010198d14e800499a544e2c43a5724e2b8d"
98+
"digest": "sha256:e2b2980ba7887f18c8d245e18a926fd52efc1abab7d10493e7a898f997ca6963"
9999
}

scripts/builtin_tools.js

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,83 @@
55
*/
66

77
export const BUILTIN_TOOLS = [
8+
{
9+
slug: 'youtube-url-normalize',
10+
name: 'YouTube 链接规范化',
11+
category: '视频',
12+
io: { input: 'text', output: 'text' },
13+
desc: '从 YouTube watch、短链接、Shorts、embed 或 live 链接中提取视频 ID,并生成规范链接。',
14+
sampleIn: 'https://youtu.be/dQw4w9WgXcQ?t=42',
15+
params: [],
16+
source: `function transform(input, params, ctx) {
17+
const raw = String(input || '').trim();
18+
if (!raw) throw new Error('请输入 YouTube URL 或 11 位视频 ID');
19+
let videoId = '';
20+
if (/^[A-Za-z0-9_-]{11}$/.test(raw)) videoId = raw;
21+
else {
22+
let url;
23+
try { url = new URL(raw); } catch { throw new Error('无效的 YouTube URL'); }
24+
const host = url.hostname.toLowerCase().replace(/^www\\./, '');
25+
if (host === 'youtu.be') videoId = url.pathname.split('/').filter(Boolean)[0] || '';
26+
else if (host === 'youtube.com' || host === 'm.youtube.com') {
27+
if (url.pathname === '/watch') videoId = url.searchParams.get('v') || '';
28+
else {
29+
const parts = url.pathname.split('/').filter(Boolean);
30+
if (['shorts', 'embed', 'live', 'v'].includes(parts[0])) videoId = parts[1] || '';
31+
}
32+
} else throw new Error('仅支持 youtube.com、m.youtube.com 和 youtu.be');
33+
}
34+
if (!/^[A-Za-z0-9_-]{11}$/.test(videoId)) throw new Error('无法提取有效的 YouTube 视频 ID');
35+
return JSON.stringify({ schemaVersion: 'toolkit.youtube-state.v1', videoId,
36+
canonicalUrl: 'https://www.youtube.com/watch?v=' + videoId });
37+
}`,
38+
},
39+
40+
{
41+
slug: 'youtube-oembed-read',
42+
name: 'YouTube 公开信息获取',
43+
category: '视频',
44+
io: { input: 'text', output: 'text' },
45+
desc: '通过 YouTube oEmbed 获取公开标题、作者、频道、缩略图和嵌入信息,无需 API Key。',
46+
sampleIn: '{"schemaVersion":"toolkit.youtube-state.v1","videoId":"dQw4w9WgXcQ","canonicalUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}',
47+
params: [],
48+
sandbox: { net: true, netHosts: ['www.youtube.com'], timeoutMs: 10000 },
49+
flags: ['net'],
50+
source: `async function transform(input, params, ctx) {
51+
let state;
52+
try { state = JSON.parse(String(input || '')); } catch { throw new Error('需要先执行 YouTube 链接规范化'); }
53+
if (state.schemaVersion !== 'toolkit.youtube-state.v1' || !state.videoId) throw new Error('无效的 YouTube workflow 状态');
54+
const endpoint = 'https://www.youtube.com/oembed?format=json&url=' + encodeURIComponent(state.canonicalUrl);
55+
const data = await ctx.http(endpoint, { method: 'GET', timeoutMs: 8000, maxResponseBytes: 262144 });
56+
if (!data || typeof data !== 'object') throw new Error('YouTube oEmbed 返回格式无效');
57+
return JSON.stringify({ ...state, metadata: { source: 'youtube-oembed',
58+
title: String(data.title || ''), authorName: String(data.author_name || ''),
59+
authorUrl: String(data.author_url || ''), thumbnailUrl: String(data.thumbnail_url || ''),
60+
thumbnailWidth: Number(data.thumbnail_width || 0), thumbnailHeight: Number(data.thumbnail_height || 0),
61+
embedHtml: String(data.html || '') } });
62+
}`,
63+
},
64+
65+
{
66+
slug: 'youtube-info-format',
67+
name: 'YouTube 信息格式化',
68+
category: '视频',
69+
io: { input: 'text', output: 'key-value' },
70+
desc: '将 YouTube workflow 状态格式化为可读文本和键值视图;不可用字段会被省略。',
71+
sampleIn: '{"schemaVersion":"toolkit.youtube-state.v1","videoId":"dQw4w9WgXcQ","canonicalUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","metadata":{"title":"Example"}}',
72+
params: [],
73+
source: `function transform(input, params, ctx) {
74+
let state;
75+
try { state = JSON.parse(String(input || '')); } catch { throw new Error('需要 JSON workflow 状态'); }
76+
const m = state.metadata || {};
77+
const rows = [['标题', String(m.title || '')], ['作者', String(m.authorName || '')],
78+
['频道主页', String(m.authorUrl || '')], ['视频 ID', String(state.videoId || '')],
79+
['规范链接', String(state.canonicalUrl || '')], ['缩略图', String(m.thumbnailUrl || '')],
80+
['嵌入代码', String(m.embedHtml || '')]].filter((row) => row[1] !== '');
81+
return { text: rows.map((row) => row[0] + ': ' + row[1]).join('\\n'), view: { kind: 'kv', rows } };
82+
}`,
83+
},
84+
885
{
986
slug: 'json-pretty',
1087
name: 'JSON 美化 / 排序',
@@ -690,6 +767,224 @@ export const BUILTIN_TOOLS = [
690767
if (hit[0] === '') re.lastIndex++;
691768
}
692769
return count ? '共 ' + count + ' 处匹配\\n' + out.join('\\n') : '无匹配';
770+
}`,
771+
},
772+
773+
{
774+
slug: 'image-data-url-inspect',
775+
name: '图片信息读取',
776+
category: '图像',
777+
io: { input: 'image', output: 'image' },
778+
desc: '读取上传或粘贴的图片(base64 data URL),纯 JS 解析 PNG/GIF/JPEG 的宽高与字节数,并把原图回传以便预览和下载。',
779+
sampleIn: '',
780+
params: [],
781+
source: `function transform(input, params, ctx) {
782+
const raw = String(input || '').trim();
783+
const m = raw.match(/^data:([\\w.+-]+\\/[\\w.+-]+);base64,(.*)$/);
784+
if (!m) throw new Error('请输入图片 data URL(data:image/...;base64,...),可直接粘贴、拖入或上传图片');
785+
const mediaType = m[1].toLowerCase();
786+
if (mediaType.indexOf('image/') !== 0) throw new Error('仅支持图片输入(image/*)');
787+
const b64 = m[2].replace(/\\s+/g, '');
788+
const bin = atob(b64);
789+
const bytes = new Uint8Array(bin.length);
790+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
791+
const dv = new DataView(bytes.buffer);
792+
let w = 0, h = 0, kind = '';
793+
if (bytes[0] === 0x89 && bytes[1] === 0x50) {
794+
kind = 'PNG'; w = dv.getUint32(16); h = dv.getUint32(20);
795+
} else if (bin.slice(0, 6) === 'GIF87a' || bin.slice(0, 6) === 'GIF89a') {
796+
kind = 'GIF'; w = dv.getUint16(6, true); h = dv.getUint16(8, true);
797+
} else if (bytes[0] === 0xFF && bytes[1] === 0xD8) {
798+
kind = 'JPEG';
799+
let i = 2;
800+
while (i + 4 < bytes.length) {
801+
if (bytes[i] !== 0xFF) { i++; continue; }
802+
const marker = bytes[i + 1];
803+
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
804+
h = dv.getUint16(i + 5); w = dv.getUint16(i + 7); break;
805+
}
806+
i += 2 + dv.getUint16(i + 2);
807+
}
808+
} else {
809+
kind = (mediaType.split('/')[1] || 'image').toUpperCase();
810+
}
811+
const ext = kind === 'JPEG' ? 'jpg' : kind.toLowerCase();
812+
const text = '图片信息' + '\\n' + [
813+
'类型: ' + kind + ' (' + mediaType + ')',
814+
'尺寸: ' + (w ? (w + ' × ' + h + ' px') : '未知(该格式未解析尺寸)'),
815+
'大小: ' + bytes.length + ' bytes',
816+
].join('\\n');
817+
return {
818+
text: text,
819+
files: [{ name: 'image.' + ext, mediaType: mediaType, dataUrl: raw }],
820+
};
821+
}`,
822+
},
823+
824+
{
825+
slug: 'qrcode-generate',
826+
name: '二维码生成',
827+
category: '图像',
828+
io: { input: 'text', output: 'image' },
829+
desc: '把文字或链接生成可扫描的二维码(自动选择 v1–v10 版本、字节模式、L 容错),输出为 SVG 图片,可直接预览或下载。纯 JS 实现,无外部依赖。',
830+
sampleIn: 'https://toolkit.fun',
831+
params: [],
832+
source: `function transform(input, params, ctx) {
833+
// QR Code 版本 1-10、字节模式、纠错级 L(表格按版本索引,0 => v1)
834+
var BLOCKS = [1, 1, 1, 1, 1, 2, 2, 2, 2, 4]; // EC 分块数
835+
var ECPER = [7, 10, 15, 20, 26, 18, 20, 24, 30, 18]; // 每块 EC 码字数
836+
var DATA = [19, 34, 55, 80, 108, 136, 156, 194, 232, 274]; // 数据码字数
837+
var CAP = [17, 32, 53, 78, 106, 134, 154, 192, 230, 271]; // 最大字节负载
838+
var ALIGN = [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50]];
839+
var VINFO = { 7: 0x07C94, 8: 0x085BC, 9: 0x09A99, 10: 0x0A4D3 };
840+
841+
function build(text) {
842+
var bytes = new TextEncoder().encode(text);
843+
var version = 0;
844+
for (var v = 0; v < 10; v++) { if (bytes.length <= CAP[v]) { version = v + 1; break; } }
845+
if (!version) throw new Error('too_long');
846+
var vi = version - 1;
847+
var N = 17 + 4 * version;
848+
var dataTotal = DATA[vi], ecPer = ECPER[vi], nBlocks = BLOCKS[vi];
849+
var ccBits = version >= 10 ? 16 : 8;
850+
851+
var M = [], R = [];
852+
for (var i = 0; i < N; i++) { M.push(new Array(N).fill(0)); R.push(new Array(N).fill(false)); }
853+
function inB(r, c) { return r >= 0 && r < N && c >= 0 && c < N; }
854+
function putF(r, c, val) { if (inB(r, c)) { M[r][c] = val ? 1 : 0; R[r][c] = true; } }
855+
function resv(r, c) { if (inB(r, c)) R[r][c] = true; }
856+
857+
function finder(r0, c0) {
858+
for (var dr = -1; dr <= 7; dr++) for (var dc = -1; dc <= 7; dc++) {
859+
var r = r0 + dr, c = c0 + dc; if (!inB(r, c)) continue;
860+
var val = 0;
861+
if (dr >= 0 && dr <= 6 && (dc === 0 || dc === 6)) val = 1;
862+
else if (dc >= 0 && dc <= 6 && (dr === 0 || dr === 6)) val = 1;
863+
else if (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4) val = 1;
864+
putF(r, c, val);
865+
}
866+
}
867+
finder(0, 0); finder(0, N - 7); finder(N - 7, 0);
868+
for (var t = 8; t < N - 8; t++) { var tv = (t % 2 === 0) ? 1 : 0; putF(t, 6, tv); putF(6, t, tv); }
869+
870+
var ac = ALIGN[vi];
871+
for (var ai = 0; ai < ac.length; ai++) for (var aj = 0; aj < ac.length; aj++) {
872+
if ((ai === 0 && aj === 0) || (ai === 0 && aj === ac.length - 1) || (ai === ac.length - 1 && aj === 0)) continue;
873+
var ar = ac[ai], acx = ac[aj];
874+
for (var dr = -2; dr <= 2; dr++) for (var dc = -2; dc <= 2; dc++) {
875+
var dark = (dr === -2 || dr === 2 || dc === -2 || dc === 2 || (dr === 0 && dc === 0)) ? 1 : 0;
876+
putF(ar + dr, acx + dc, dark);
877+
}
878+
}
879+
putF(N - 8, 8, 1);
880+
881+
for (var fi = 0; fi < 15; fi++) {
882+
if (fi < 6) resv(fi, 8); else if (fi < 8) resv(fi + 1, 8); else resv(N - 15 + fi, 8);
883+
if (fi < 8) resv(8, N - fi - 1); else if (fi < 9) resv(8, 15 - fi - 1 + 1); else resv(8, 15 - fi - 1);
884+
}
885+
var vb = null;
886+
if (version >= 7) {
887+
vb = VINFO[version];
888+
for (var k = 0; k < 18; k++) { var vr = Math.floor(k / 3), vcc = k % 3 + N - 11; resv(vr, vcc); resv(vcc, vr); }
889+
}
890+
891+
var bits = [];
892+
function push(val, n) { for (var k = n - 1; k >= 0; k--) bits.push((val >>> k) & 1); }
893+
push(0x4, 4); // 字节模式
894+
push(bytes.length, ccBits); // 字符计数
895+
for (var bi = 0; bi < bytes.length; bi++) push(bytes[bi], 8);
896+
var term = Math.min(4, dataTotal * 8 - bits.length);
897+
for (var te = 0; te < term; te++) bits.push(0);
898+
while (bits.length % 8 !== 0) bits.push(0);
899+
var dataCw = [];
900+
for (var b = 0; b + 8 <= bits.length; b += 8) { var by = 0; for (var k2 = 0; k2 < 8; k2++) by = (by << 1) | bits[b + k2]; dataCw.push(by); }
901+
var pads = [0xEC, 0x11], pi = 0;
902+
while (dataCw.length < dataTotal) dataCw.push(pads[pi++ % 2]);
903+
dataCw = dataCw.slice(0, dataTotal);
904+
905+
var EXP = new Uint8Array(512), LOG = new Uint8Array(256), xx = 1;
906+
for (var e = 0; e < 255; e++) { EXP[e] = xx; LOG[xx] = e; xx <<= 1; if (xx & 0x100) xx ^= 0x11D; }
907+
for (e = 255; e < 512; e++) EXP[e] = EXP[e - 255];
908+
function gmul(a, b) { if (!a || !b) return 0; return EXP[LOG[a] + LOG[b]]; }
909+
function polyMul(p1, p2) { var o = new Array(p1.length + p2.length - 1).fill(0);
910+
for (var x = 0; x < p1.length; x++) for (var y = 0; y < p2.length; y++) o[x + y] ^= gmul(p1[x], p2[y]); return o; }
911+
function rsEc(darr, deg) {
912+
var gen = [1];
913+
for (var gi = 0; gi < deg; gi++) gen = polyMul(gen, [1, EXP[gi]]);
914+
var rem = darr.slice().concat(new Array(deg).fill(0));
915+
while (rem.length - gen.length >= 0) {
916+
var cf = rem[0];
917+
for (var gj = 0; gj < gen.length; gj++) rem[gj] ^= gmul(gen[gj], cf);
918+
var go = 0; while (go < rem.length && rem[go] === 0) go++; rem = rem.slice(go);
919+
}
920+
while (rem.length < deg) rem.unshift(0);
921+
return rem.slice(-deg);
922+
}
923+
924+
var base = Math.floor(dataTotal / nBlocks), remN = dataTotal - base * nBlocks;
925+
var blocks = [], idx = 0;
926+
for (var bl = 0; bl < nBlocks; bl++) {
927+
var dlen = base + (bl >= nBlocks - remN ? 1 : 0);
928+
var darr = dataCw.slice(idx, idx + dlen); idx += dlen;
929+
blocks.push({ d: darr, e: rsEc(darr, ecPer) });
930+
}
931+
var cw = [], maxD = 0;
932+
for (bl = 0; bl < nBlocks; bl++) if (blocks[bl].d.length > maxD) maxD = blocks[bl].d.length;
933+
for (var ii = 0; ii < maxD; ii++) for (bl = 0; bl < nBlocks; bl++) if (ii < blocks[bl].d.length) cw.push(blocks[bl].d[ii]);
934+
for (ii = 0; ii < ecPer; ii++) for (bl = 0; bl < nBlocks; bl++) cw.push(blocks[bl].e[ii]);
935+
936+
var bitIndex = 7, byteIndex = 0, inc = -1, row = N - 1;
937+
for (var col = N - 1; col > 0; col -= 2) {
938+
if (col === 6) col--;
939+
while (true) {
940+
for (var c = 0; c < 2; c++) {
941+
var cc = col - c;
942+
if (!R[row][cc]) {
943+
var dark = false;
944+
if (byteIndex < cw.length) dark = (((cw[byteIndex] >>> bitIndex) & 1) === 1);
945+
if (((row + cc) & 1) === 0) dark = !dark; // 掩码图案 0
946+
M[row][cc] = dark ? 1 : 0;
947+
bitIndex--; if (bitIndex === -1) { byteIndex++; bitIndex = 7; }
948+
}
949+
}
950+
row += inc;
951+
if (row < 0 || N <= row) { row -= inc; inc = -inc; break; }
952+
}
953+
}
954+
var FMT = 0x77C4; // ECC-L + 掩码0 的格式信息(所有版本相同)
955+
for (var fi2 = 0; fi2 < 15; fi2++) {
956+
var fm = ((FMT >>> fi2) & 1) === 1 ? 1 : 0;
957+
if (fi2 < 6) M[fi2][8] = fm; else if (fi2 < 8) M[fi2 + 1][8] = fm; else M[N - 15 + fi2][8] = fm;
958+
if (fi2 < 8) M[8][N - fi2 - 1] = fm; else if (fi2 < 9) M[8][15 - fi2 - 1 + 1] = fm; else M[8][15 - fi2 - 1] = fm;
959+
}
960+
M[N - 8][8] = 1;
961+
if (vb !== null) {
962+
for (k = 0; k < 18; k++) {
963+
var vbit = ((vb >>> k) & 1) === 1 ? 1 : 0;
964+
M[Math.floor(k / 3)][k % 3 + N - 11] = vbit;
965+
M[k % 3 + N - 11][Math.floor(k / 3)] = vbit;
966+
}
967+
}
968+
return M;
969+
}
970+
971+
function toSvg(M) {
972+
var N = M.length, Q = 6, QUIET = 4, dim = (N + QUIET * 2) * Q;
973+
var s = '<svg xmlns="http://www.w3.org/2000/svg" width="' + dim + '" height="' + dim + '" viewBox="0 0 ' + dim + ' ' + dim + '">';
974+
s += '<rect width="' + dim + '" height="' + dim + '" fill="#ffffff"/>';
975+
for (var r = 0; r < N; r++) for (var c = 0; c < N; c++) {
976+
if (M[r][c]) { var x = (c + QUIET) * Q, y = (r + QUIET) * Q; s += '<rect x="' + x + '" y="' + y + '" width="' + Q + '" height="' + Q + '" fill="#000000"/>'; }
977+
}
978+
s += '</svg>'; return s;
979+
}
980+
981+
var text = String((params && params.text != null) ? params.text : (input == null ? '' : input));
982+
text = text.replace(/^\\s+|\\s+$/g, '');
983+
if (!text) return '请输入要生成二维码的内容(文字或链接)';
984+
var M;
985+
try { M = build(text); }
986+
catch (e) { if (e && e.message === 'too_long') return '内容太长:最多约 270 个英文 / 90 个汉字,请缩短后再试'; throw e; }
987+
return { files: [{ name: 'qrcode.svg', mediaType: 'image/svg+xml', dataUrl: 'data:image/svg+xml;base64,' + btoa(toSvg(M)) }] };
693988
}`,
694989
},
695990
];

0 commit comments

Comments
 (0)