@bash0816/copilot-termux 1.0.63
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/LICENSE +683 -0
- package/README.md +76 -0
- package/THIRD-PARTY-LICENSES/COPILOT-LICENSE.md +35 -0
- package/THIRD-PARTY-LICENSES/GCC-RUNTIME-EXCEPTION.txt +73 -0
- package/THIRD-PARTY-LICENSES/GPL-3.0.txt +674 -0
- package/THIRD-PARTY-LICENSES/LGPL-2.1.txt +501 -0
- package/THIRD-PARTY-LICENSES/NODE-LICENSE.txt +2946 -0
- package/THIRD-PARTY-LICENSES/NODE-PTY-LICENSE.txt +69 -0
- package/THIRD-PARTY-NOTICES.md +66 -0
- package/bin/copilot +70 -0
- package/bin/copilot-termux +9 -0
- package/config/manifest.json +7 -0
- package/lib/bionic-compat.so +0 -0
- package/lib/native/pty.node +0 -0
- package/lib/platform-patch.js +451 -0
- package/lib/setup.js +143 -0
- package/package.json +24 -0
- package/scripts/bionic-compat.c +82 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com)
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
MIT License
|
|
48
|
+
|
|
49
|
+
Copyright (c) 2018 - present Microsoft Corporation
|
|
50
|
+
|
|
51
|
+
All rights reserved.
|
|
52
|
+
|
|
53
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
54
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
55
|
+
in the Software without restriction, including without limitation the rights
|
|
56
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
57
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
58
|
+
furnished to do so, subject to the following conditions:
|
|
59
|
+
|
|
60
|
+
The above copyright notice and this permission notice shall be included in all
|
|
61
|
+
copies or substantial portions of the Software.
|
|
62
|
+
|
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
64
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
65
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
66
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
67
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
68
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
69
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
This package includes third-party software with the following licenses.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## GitHub Copilot CLI
|
|
8
|
+
|
|
9
|
+
- Source: https://github.com/github/copilot-cli (private)
|
|
10
|
+
- Version: 1.0.63
|
|
11
|
+
- License: GitHub Copilot CLI License
|
|
12
|
+
- License text: THIRD-PARTY-LICENSES/COPILOT-LICENSE.md
|
|
13
|
+
|
|
14
|
+
Redistributed in unmodified form as part of this package, in accordance with
|
|
15
|
+
Section 2 of the GitHub Copilot CLI License.
|
|
16
|
+
|
|
17
|
+
Files: lib/copilot/
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Node.js
|
|
22
|
+
|
|
23
|
+
- Source: https://nodejs.org/
|
|
24
|
+
- License: MIT (main), with bundled components under various licenses
|
|
25
|
+
(see THIRD-PARTY-LICENSES/NODE-LICENSE.txt for full attribution)
|
|
26
|
+
- License text: THIRD-PARTY-LICENSES/NODE-LICENSE.txt
|
|
27
|
+
|
|
28
|
+
Files: lib/node
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## GNU C Library (glibc)
|
|
33
|
+
|
|
34
|
+
- Source: https://www.gnu.org/software/libc/
|
|
35
|
+
- License: LGPL-2.1+
|
|
36
|
+
- License text: THIRD-PARTY-LICENSES/LGPL-2.1.txt
|
|
37
|
+
|
|
38
|
+
Files:
|
|
39
|
+
lib/glibc/ld-linux-aarch64.so.1
|
|
40
|
+
lib/glibc/libc.so.6
|
|
41
|
+
lib/glibc/libm.so.6
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## GCC Runtime Libraries
|
|
46
|
+
|
|
47
|
+
- Source: https://gcc.gnu.org/
|
|
48
|
+
- License: GPL-3.0 WITH GCC Runtime Library Exception
|
|
49
|
+
- Exception text: THIRD-PARTY-LICENSES/GCC-RUNTIME-EXCEPTION.txt
|
|
50
|
+
- Base license text: THIRD-PARTY-LICENSES/GPL-3.0.txt
|
|
51
|
+
|
|
52
|
+
Files:
|
|
53
|
+
lib/glibc/libgcc_s.so.1
|
|
54
|
+
lib/glibc/libstdc++.so.6
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## node-pty
|
|
59
|
+
|
|
60
|
+
- Source: https://github.com/microsoft/node-pty
|
|
61
|
+
- Version: 1.1.0
|
|
62
|
+
- License: MIT
|
|
63
|
+
- License text: THIRD-PARTY-LICENSES/NODE-PTY-LICENSE.txt
|
|
64
|
+
|
|
65
|
+
Compiled natively for Android aarch64 (bionic) and bundled as `lib/native/pty.node`.
|
|
66
|
+
Used to provide pseudo-terminal support for the interactive TUI mode of @github/copilot.
|
package/bin/copilot
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
SELF="$0"
|
|
4
|
+
while [ -L "${SELF}" ]; do
|
|
5
|
+
DIR=$(CDPATH= cd -- "$(dirname -- "${SELF}")" && pwd)
|
|
6
|
+
SELF="$(readlink "${SELF}")"
|
|
7
|
+
case "${SELF}" in /*) ;; *) SELF="${DIR}/${SELF}" ;; esac
|
|
8
|
+
done
|
|
9
|
+
PACKAGE_DIR=$(CDPATH= cd -- "$(dirname -- "${SELF}")/.." && pwd)
|
|
10
|
+
_COMPAT="${PACKAGE_DIR}/lib/bionic-compat.so"
|
|
11
|
+
[ -f "${_COMPAT}" ] || { echo '[copilot-termux] ERROR: lib/bionic-compat.so missing. Reinstall package.' >&2; exit 1; }
|
|
12
|
+
PRELOAD="${_COMPAT}${LD_PRELOAD:+:${LD_PRELOAD}}"
|
|
13
|
+
|
|
14
|
+
CACHE_DIR="${HOME}/.copilot-termux"
|
|
15
|
+
CURRENT="${CACHE_DIR}/current"
|
|
16
|
+
|
|
17
|
+
if [ ! -d "${CURRENT}" ]; then
|
|
18
|
+
echo "[copilot-termux] Not set up. Run: copilot-termux setup" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
COPILOT_INDEX="${CURRENT}/index.js"
|
|
23
|
+
if [ ! -f "${COPILOT_INDEX}" ]; then
|
|
24
|
+
echo "[copilot-termux] Installation broken. Run: copilot-termux setup" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
MXC_WRAP="${CACHE_DIR}/mxc-wrap"
|
|
29
|
+
if [ -d "${MXC_WRAP}/arm64" ] && [ -x "${MXC_WRAP}/arm64/lxc-exec" ]; then
|
|
30
|
+
export MXC_BIN_DIR="${MXC_WRAP}"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [ -n "${MAGI_NODE:-}" ]; then
|
|
34
|
+
MAGI_NODE_VERSION="$("${MAGI_NODE}" --version 2>/dev/null || true)"
|
|
35
|
+
MAGI_NODE_MAJOR=""
|
|
36
|
+
if [ -n "${MAGI_NODE_VERSION}" ]; then
|
|
37
|
+
MAGI_NODE_VERSION="${MAGI_NODE_VERSION#v}"
|
|
38
|
+
MAGI_NODE_MAJOR="${MAGI_NODE_VERSION%%.*}"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
case "${MAGI_NODE_MAJOR}" in
|
|
42
|
+
''|*[!0-9]*)
|
|
43
|
+
echo "[copilot-termux] MAGI_NODE version check failed; falling back to node" >&2
|
|
44
|
+
LD_PRELOAD="${PRELOAD}" exec node \
|
|
45
|
+
--require "${PACKAGE_DIR}/lib/platform-patch.js" \
|
|
46
|
+
"${COPILOT_INDEX}" \
|
|
47
|
+
"$@"
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
if [ "${MAGI_NODE_MAJOR}" -lt 24 ]; then
|
|
52
|
+
echo "[copilot-termux] MAGI_NODE ${MAGI_NODE_VERSION} is below v24; falling back to node" >&2
|
|
53
|
+
LD_PRELOAD="${PRELOAD}" exec node \
|
|
54
|
+
--require "${PACKAGE_DIR}/lib/platform-patch.js" \
|
|
55
|
+
"${COPILOT_INDEX}" \
|
|
56
|
+
"$@"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
exec env -u LD_LIBRARY_PATH \
|
|
60
|
+
LD_PRELOAD="${PRELOAD}" \
|
|
61
|
+
"${MAGI_NODE}" \
|
|
62
|
+
--require "${PACKAGE_DIR}/lib/platform-patch.js" \
|
|
63
|
+
"${COPILOT_INDEX}" \
|
|
64
|
+
"$@"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
LD_PRELOAD="${PRELOAD}" exec node \
|
|
68
|
+
--require "${PACKAGE_DIR}/lib/platform-patch.js" \
|
|
69
|
+
"${COPILOT_INDEX}" \
|
|
70
|
+
"$@"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { setup } = require('../lib/setup');
|
|
4
|
+
const cmd = process.argv[2];
|
|
5
|
+
if (cmd === 'setup' || cmd === 'update') {
|
|
6
|
+
setup().then(() => process.exit(0)).catch(e => { console.error(e.message); process.exit(1); });
|
|
7
|
+
} else {
|
|
8
|
+
console.log('Usage: copilot-termux <setup|update>');
|
|
9
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Override process.platform so @github/copilot treats Android as Linux.
|
|
3
|
+
// @github/copilot is distributed unmodified (LICENSE Section 2).
|
|
4
|
+
// This file is our code and is NOT part of the @github/copilot software.
|
|
5
|
+
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
6
|
+
|
|
7
|
+
if (process.report) {
|
|
8
|
+
const orig = process.report.getReport.bind(process.report);
|
|
9
|
+
process.report.getReport = (...args) => {
|
|
10
|
+
const r = orig(...args);
|
|
11
|
+
if (r && r.header) r.header.platform = 'linux';
|
|
12
|
+
return r;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Redirect linux-arm64 native addons to bionic-compatible variants.
|
|
17
|
+
// bionic Node.js cannot load glibc addons; glibc fallback causes segfault.
|
|
18
|
+
const Module = require('module');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// Bundled Termux-native pty.node (built against bionic, not glibc).
|
|
23
|
+
const NATIVE_PTY = path.join(__dirname, 'native', 'pty.node');
|
|
24
|
+
|
|
25
|
+
const origResolve = Module._resolveFilename.bind(Module);
|
|
26
|
+
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
27
|
+
if (typeof request === 'string' && request.endsWith('.node')) {
|
|
28
|
+
// Redirect pty.node to our bundled Termux-native build.
|
|
29
|
+
if (path.basename(request) === 'pty.node' &&
|
|
30
|
+
(request.includes('linux-arm64') || request.includes('linuxmusl-arm64'))) {
|
|
31
|
+
if (fs.existsSync(NATIVE_PTY)) return NATIVE_PTY;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Redirect other linux-arm64 addons to linuxmusl-arm64 variants.
|
|
35
|
+
if (request.includes('linux-arm64')) {
|
|
36
|
+
const muslReq = request.replace(/linux-arm64/g, 'linuxmusl-arm64');
|
|
37
|
+
try {
|
|
38
|
+
const resolved = origResolve(muslReq, parent, isMain, options);
|
|
39
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (e.code !== 'MODULE_NOT_FOUND') throw e;
|
|
42
|
+
}
|
|
43
|
+
// Block glibc fallback — loading linux-arm64 (glibc) on bionic causes segfault.
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[copilot-termux] Cannot load glibc addon on bionic: ${request}\n` +
|
|
46
|
+
'No linuxmusl-arm64 variant available. This addon is not supported on Android.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return origResolve(request, parent, isMain, options);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// [Android bionic 対応] linuxmusl-arm64/runtime.node は Rust tokio を使う。
|
|
54
|
+
// bionic 上で musl pthread ABI でスレッドを生成すると TUI 起動時に SIGSEGV。
|
|
55
|
+
// runtime.node ロード後に Rust async 初期化関数を no-op に差し替えて阻止する。
|
|
56
|
+
const origLoad = Module._load.bind(Module);
|
|
57
|
+
Module._load = function (request, parent, isMain) {
|
|
58
|
+
const result = origLoad(request, parent, isMain);
|
|
59
|
+
if (typeof request === 'string' &&
|
|
60
|
+
path.basename(request) === 'runtime.node') {
|
|
61
|
+
// Rust tokio を使う関数群を no-op に差し替え。
|
|
62
|
+
// sessionStore*/sessionSqlite* は非同期 SQLite (tokio)、
|
|
63
|
+
// modelHttp*/networkFetch*/ahpRelay*/websocketResponses* は Rust HTTP (tokio)。
|
|
64
|
+
// featureFlagService* は同期 Rust のため除外(no-op にすると .handle クラッシュ)。
|
|
65
|
+
const TOKIO_PATTERN = /^(modelHttp|networkFetch|ahpRelay|websocketResponses|sessionStore|sessionSqlite)/;
|
|
66
|
+
for (const key of Object.keys(result)) {
|
|
67
|
+
if (TOKIO_PATTERN.test(key) && typeof result[key] === 'function') {
|
|
68
|
+
result[key] = () => undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof result.networkFetchGetExtraCaPems === 'function') {
|
|
72
|
+
result.networkFetchGetExtraCaPems = () => ({ errors: [], pems: [] });
|
|
73
|
+
}
|
|
74
|
+
// capiClientListModels を Node.js fetch で実装(Rust tokio SIGSEGV 回避)
|
|
75
|
+
if (typeof result.capiClientListModels === 'function') {
|
|
76
|
+
result.capiClientListModels = async function(handle, _includeHidden, _skipCache, _applyModelLimitCaps, _networkingConfigId) {
|
|
77
|
+
let authHeaders;
|
|
78
|
+
try {
|
|
79
|
+
const prepared = result.capiClientPrepareRequestHeaders(handle, '', []);
|
|
80
|
+
authHeaders = {};
|
|
81
|
+
for (const {name, value} of prepared.headers) {
|
|
82
|
+
authHeaders[name] = value;
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
throw new Error(JSON.stringify({kind: 'network', message: `prepareHeaders failed: ${e.message}`}));
|
|
86
|
+
}
|
|
87
|
+
const baseUrl = 'https://api.githubcopilot.com';
|
|
88
|
+
let res;
|
|
89
|
+
try {
|
|
90
|
+
res = await globalThis.fetch(`${baseUrl}/models`, {method: 'GET', headers: authHeaders});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new Error(JSON.stringify({kind: 'network', message: e.message}));
|
|
93
|
+
}
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const body = await res.text().catch(() => '');
|
|
96
|
+
const hdrs = [...res.headers.entries()].map(([name, value]) => ({name, value}));
|
|
97
|
+
throw new Error(JSON.stringify({kind: 'http', status: res.status, statusText: res.statusText, body, headers: hdrs, hasRequestId: res.headers.has('x-request-id')}));
|
|
98
|
+
}
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
const models = Array.isArray(data) ? data : (data.data ?? data.models ?? []);
|
|
101
|
+
const rateHeaders = [...res.headers.entries()].map(([name, value]) => ({name, value}));
|
|
102
|
+
return {modelsJson: JSON.stringify(models), copilotUrl: baseUrl, usageRatelimitHeaders: rateHeaders, capturedAssignmentContext: undefined};
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (typeof result.sessionSqliteOpen === 'function') {
|
|
106
|
+
result.sessionSqliteOpen = () => 1;
|
|
107
|
+
}
|
|
108
|
+
if (typeof result.sessionSqliteQuery === 'function') {
|
|
109
|
+
result.sessionSqliteQuery = () => ({ rows: '[]' });
|
|
110
|
+
}
|
|
111
|
+
if (typeof result.sessionSqliteRun === 'function') {
|
|
112
|
+
result.sessionSqliteRun = () => ({ rowsAffected: 0, lastInsertRowid: null });
|
|
113
|
+
}
|
|
114
|
+
if (typeof result.sessionSqliteFileExists === 'function') {
|
|
115
|
+
result.sessionSqliteFileExists = () => false;
|
|
116
|
+
}
|
|
117
|
+
// --- JS networkFetch* implementation (bionic: tokio networkFetch* are no-op'd) ---
|
|
118
|
+
// B7 が QXe() から直接呼ばれる MCP 専用パスでクラッシュするため JS で代替。
|
|
119
|
+
// networkFetchStreamStart → { requestId, response: Promise<{handle,url,status,statusText,headers}> }
|
|
120
|
+
// networkFetchStreamRead → Promise<{ done, body?: Uint8Array }>
|
|
121
|
+
// networkFetchStreamClose → void
|
|
122
|
+
// networkFetchRequestCancel → void
|
|
123
|
+
const _nfMap = new Map();
|
|
124
|
+
let _nfIdSeq = 3e6;
|
|
125
|
+
|
|
126
|
+
result.networkFetchStreamStart = function(req) {
|
|
127
|
+
const requestId = 'nf-' + (++_nfIdSeq);
|
|
128
|
+
const abortCtrl = new AbortController();
|
|
129
|
+
const entry = { abort: () => abortCtrl.abort(), reader: null };
|
|
130
|
+
_nfMap.set(requestId, entry);
|
|
131
|
+
|
|
132
|
+
const headers = {};
|
|
133
|
+
if (Array.isArray(req.headers)) {
|
|
134
|
+
for (const { name, value } of req.headers) headers[name] = value;
|
|
135
|
+
} else if (req.headers && typeof req.headers === 'object') {
|
|
136
|
+
Object.assign(headers, req.headers);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const response = (async () => {
|
|
140
|
+
let res;
|
|
141
|
+
try {
|
|
142
|
+
res = await globalThis.fetch(req.url, {
|
|
143
|
+
method: req.method || 'GET',
|
|
144
|
+
headers,
|
|
145
|
+
body: req.body ?? undefined,
|
|
146
|
+
signal: abortCtrl.signal,
|
|
147
|
+
redirect: req.redirect || 'follow',
|
|
148
|
+
});
|
|
149
|
+
} catch (e) {
|
|
150
|
+
_nfMap.delete(requestId);
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
const handle = requestId;
|
|
154
|
+
const reader = res.body ? res.body.getReader() : null;
|
|
155
|
+
entry.reader = reader;
|
|
156
|
+
const respHeaders = [...res.headers.entries()].map(([name, value]) => ({ name, value }));
|
|
157
|
+
return { handle, url: res.url, status: res.status, statusText: res.statusText, headers: respHeaders };
|
|
158
|
+
})();
|
|
159
|
+
|
|
160
|
+
return { requestId, response };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
result.networkFetchStreamRead = function(handle) {
|
|
164
|
+
return (async () => {
|
|
165
|
+
const entry = _nfMap.get(handle);
|
|
166
|
+
if (!entry || !entry.reader) return { done: true };
|
|
167
|
+
try {
|
|
168
|
+
const { done, value } = await entry.reader.read();
|
|
169
|
+
if (done) { _nfMap.delete(handle); return { done: true }; }
|
|
170
|
+
return { done: false, body: value };
|
|
171
|
+
} catch (e) {
|
|
172
|
+
_nfMap.delete(handle);
|
|
173
|
+
throw e;
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
result.networkFetchStreamClose = function(handle) {
|
|
179
|
+
const entry = _nfMap.get(handle);
|
|
180
|
+
if (entry) {
|
|
181
|
+
try { entry.reader?.cancel(); } catch (_) {}
|
|
182
|
+
try { entry.abort(); } catch (_) {}
|
|
183
|
+
_nfMap.delete(handle);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
result.networkFetchRequestCancel = function(requestId) {
|
|
188
|
+
const entry = _nfMap.get(requestId);
|
|
189
|
+
if (entry) {
|
|
190
|
+
try { entry.abort(); } catch (_) {}
|
|
191
|
+
_nfMap.delete(requestId);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
result.networkFetchResetClients = function() {
|
|
196
|
+
for (const entry of _nfMap.values()) {
|
|
197
|
+
try { entry.abort(); } catch (_) {}
|
|
198
|
+
}
|
|
199
|
+
_nfMap.clear();
|
|
200
|
+
};
|
|
201
|
+
// --- end JS networkFetch* implementation ---
|
|
202
|
+
// --- JS model HTTP implementation (bionic: tokio modelHttp* are no-op'd) ---
|
|
203
|
+
// Stream store: streamId → {events, index, finalMessage}
|
|
204
|
+
const _jsStreams = new Map();
|
|
205
|
+
let _accIdSeq = 1e6;
|
|
206
|
+
// Accumulator store: accId → {message}
|
|
207
|
+
const _jsAccs = new Map();
|
|
208
|
+
|
|
209
|
+
// Helper: parse Anthropic SSE body into events array
|
|
210
|
+
function _parseAnthropicSSE(bodyText) {
|
|
211
|
+
const events = [];
|
|
212
|
+
const lines = bodyText.split(/\r\n|\r|\n/);
|
|
213
|
+
let dataLines = [];
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
if (line === '' || line === '\r') {
|
|
216
|
+
if (dataLines.length > 0) {
|
|
217
|
+
const dataStr = dataLines.join('\n');
|
|
218
|
+
dataLines = [];
|
|
219
|
+
if (dataStr.trim() === '[DONE]') continue;
|
|
220
|
+
try { events.push(JSON.parse(dataStr)); } catch(_) {}
|
|
221
|
+
}
|
|
222
|
+
} else if (line.startsWith('data: ')) {
|
|
223
|
+
dataLines.push(line.slice(6));
|
|
224
|
+
} else if (line.startsWith('data:')) {
|
|
225
|
+
dataLines.push(line.slice(5));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (dataLines.length > 0) {
|
|
229
|
+
const dataStr = dataLines.join('\n');
|
|
230
|
+
if (dataStr.trim() !== '[DONE]') {
|
|
231
|
+
try { events.push(JSON.parse(dataStr)); } catch(_) {}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return events;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Helper: reconstruct final Anthropic message from SSE events
|
|
238
|
+
function _reconstructFinalMessage(events) {
|
|
239
|
+
let message = null;
|
|
240
|
+
const contentBlocks = new Map(); // index → block
|
|
241
|
+
for (const e of events) {
|
|
242
|
+
if (e.type === 'message_start') {
|
|
243
|
+
message = {
|
|
244
|
+
id: e.message && e.message.id,
|
|
245
|
+
type: (e.message && e.message.type) || 'message',
|
|
246
|
+
role: (e.message && e.message.role) || 'assistant',
|
|
247
|
+
model: e.message && e.message.model,
|
|
248
|
+
stop_reason: (e.message && e.message.stop_reason) || null,
|
|
249
|
+
stop_sequence: (e.message && e.message.stop_sequence) || null,
|
|
250
|
+
usage: (e.message && e.message.usage) || { input_tokens: 0, output_tokens: 0 },
|
|
251
|
+
content: []
|
|
252
|
+
};
|
|
253
|
+
} else if (e.type === 'content_block_start') {
|
|
254
|
+
const blk = e.content_block;
|
|
255
|
+
if (!blk) continue;
|
|
256
|
+
if (blk.type === 'text') {
|
|
257
|
+
contentBlocks.set(e.index, { type: 'text', text: blk.text || '' });
|
|
258
|
+
} else if (blk.type === 'tool_use') {
|
|
259
|
+
contentBlocks.set(e.index, { type: 'tool_use', id: blk.id, name: blk.name, _inputBuf: '' });
|
|
260
|
+
} else if (blk.type === 'thinking') {
|
|
261
|
+
contentBlocks.set(e.index, { type: 'thinking', thinking: blk.thinking || '', signature: blk.signature || '' });
|
|
262
|
+
} else if (blk.type === 'redacted_thinking') {
|
|
263
|
+
contentBlocks.set(e.index, { type: 'redacted_thinking', data: blk.data });
|
|
264
|
+
}
|
|
265
|
+
} else if (e.type === 'content_block_delta') {
|
|
266
|
+
const blk = contentBlocks.get(e.index);
|
|
267
|
+
if (!blk || !e.delta) continue;
|
|
268
|
+
if (e.delta.type === 'text_delta') {
|
|
269
|
+
blk.text = (blk.text || '') + (e.delta.text || '');
|
|
270
|
+
} else if (e.delta.type === 'input_json_delta') {
|
|
271
|
+
blk._inputBuf = (blk._inputBuf || '') + (e.delta.partial_json || '');
|
|
272
|
+
} else if (e.delta.type === 'thinking_delta') {
|
|
273
|
+
blk.thinking = (blk.thinking || '') + (e.delta.thinking || '');
|
|
274
|
+
} else if (e.delta.type === 'signature_delta') {
|
|
275
|
+
blk.signature = (blk.signature || '') + (e.delta.signature || '');
|
|
276
|
+
}
|
|
277
|
+
} else if (e.type === 'content_block_stop') {
|
|
278
|
+
const blk = contentBlocks.get(e.index);
|
|
279
|
+
if (blk && blk.type === 'tool_use') {
|
|
280
|
+
try { blk.input = JSON.parse(blk._inputBuf || '{}'); } catch(_) { blk.input = {}; }
|
|
281
|
+
delete blk._inputBuf;
|
|
282
|
+
}
|
|
283
|
+
} else if (e.type === 'message_delta') {
|
|
284
|
+
if (message) {
|
|
285
|
+
if (e.delta && e.delta.stop_reason != null) message.stop_reason = e.delta.stop_reason;
|
|
286
|
+
if (e.delta && e.delta.stop_sequence != null) message.stop_sequence = e.delta.stop_sequence;
|
|
287
|
+
if (e.usage && e.usage.output_tokens != null && message.usage) {
|
|
288
|
+
message.usage.output_tokens = e.usage.output_tokens;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (message) {
|
|
294
|
+
const blocks = Array.from(contentBlocks.entries())
|
|
295
|
+
.sort(([a], [b]) => a - b)
|
|
296
|
+
.map(([, v]) => v);
|
|
297
|
+
message.content = blocks;
|
|
298
|
+
}
|
|
299
|
+
if (!message) {
|
|
300
|
+
message = { role: 'assistant', content: [], stop_reason: 'error', stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } };
|
|
301
|
+
}
|
|
302
|
+
return message;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 1. anthropicMessageStreamAccumulatorCreate → JS mock
|
|
306
|
+
if (typeof result.anthropicMessageStreamAccumulatorCreate === 'function') {
|
|
307
|
+
result.anthropicMessageStreamAccumulatorCreate = function(_handleJson) {
|
|
308
|
+
const id = _accIdSeq++;
|
|
309
|
+
_jsAccs.set(id, { message: null });
|
|
310
|
+
return { json: JSON.stringify({ accumulatorId: id }) };
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 2. anthropicMessageStreamAccumulatorFinish → return accumulated message
|
|
315
|
+
if (typeof result.anthropicMessageStreamAccumulatorFinish === 'function') {
|
|
316
|
+
result.anthropicMessageStreamAccumulatorFinish = function(id) {
|
|
317
|
+
const acc = _jsAccs.get(id) || { message: null };
|
|
318
|
+
_jsAccs.delete(id);
|
|
319
|
+
return { json: JSON.stringify({ message: acc.message }) };
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 3. anthropicMessageStreamAccumulatorDrop → cleanup
|
|
324
|
+
if (typeof result.anthropicMessageStreamAccumulatorDrop === 'function') {
|
|
325
|
+
result.anthropicMessageStreamAccumulatorDrop = function(id) {
|
|
326
|
+
_jsAccs.delete(id);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 4. modelHttpStreamStart → fetch full SSE body, parse events
|
|
331
|
+
result.modelHttpStreamStart = async function(jsonArg) {
|
|
332
|
+
const req = JSON.parse(jsonArg);
|
|
333
|
+
let body = req.body;
|
|
334
|
+
if (body !== null && body !== undefined && typeof body === 'object') {
|
|
335
|
+
if (body.type === 'Buffer' && Array.isArray(body.data)) {
|
|
336
|
+
body = Buffer.from(body.data);
|
|
337
|
+
} else {
|
|
338
|
+
body = JSON.stringify(body);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
let res;
|
|
342
|
+
try {
|
|
343
|
+
res = await globalThis.fetch(req.url, {
|
|
344
|
+
method: req.method || 'POST',
|
|
345
|
+
headers: req.headers || {},
|
|
346
|
+
body: body,
|
|
347
|
+
});
|
|
348
|
+
} catch(e) {
|
|
349
|
+
throw new Error(JSON.stringify({ kind: 'network', message: e.message }));
|
|
350
|
+
}
|
|
351
|
+
const headers = {};
|
|
352
|
+
for (const [k, v] of res.headers.entries()) headers[k] = v;
|
|
353
|
+
const bodyText = await res.text().catch(() => '');
|
|
354
|
+
if (res.status < 200 || res.status >= 300) {
|
|
355
|
+
return { json: JSON.stringify({ bodyText, status: res.status, statusText: res.statusText, headers, streamId: null }) };
|
|
356
|
+
}
|
|
357
|
+
const events = _parseAnthropicSSE(bodyText);
|
|
358
|
+
const finalMessage = _reconstructFinalMessage(events);
|
|
359
|
+
const streamId = 'js-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
|
|
360
|
+
_jsStreams.set(streamId, { events, index: 0, finalMessage });
|
|
361
|
+
return { json: JSON.stringify({ bodyText: null, status: res.status, statusText: res.statusText, headers, streamId }) };
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// 5. modelHttpStreamNextAnthropicMessageEvent → yield events one by one
|
|
365
|
+
result.modelHttpStreamNextAnthropicMessageEvent = async function(streamId, accId) {
|
|
366
|
+
const st = _jsStreams.get(streamId);
|
|
367
|
+
if (!st || st.index >= st.events.length) {
|
|
368
|
+
if (st) {
|
|
369
|
+
const acc = _jsAccs.get(accId);
|
|
370
|
+
if (acc) acc.message = st.finalMessage;
|
|
371
|
+
_jsStreams.delete(streamId);
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return { json: JSON.stringify({ kind: 'ok', processResult: { event: st.events[st.index++] } }) };
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// 6. modelHttpStreamCancel → cleanup
|
|
379
|
+
result.modelHttpStreamCancel = function(streamId) {
|
|
380
|
+
_jsStreams.delete(streamId);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// 7. modelHttpRequest → non-streaming fetch
|
|
384
|
+
result.modelHttpRequest = async function(jsonArg) {
|
|
385
|
+
const req = JSON.parse(jsonArg);
|
|
386
|
+
let body = req.body;
|
|
387
|
+
if (body !== null && body !== undefined && typeof body === 'object') {
|
|
388
|
+
if (body.type === 'Buffer' && Array.isArray(body.data)) {
|
|
389
|
+
body = Buffer.from(body.data);
|
|
390
|
+
} else {
|
|
391
|
+
body = JSON.stringify(body);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
let res;
|
|
395
|
+
try {
|
|
396
|
+
res = await globalThis.fetch(req.url, {
|
|
397
|
+
method: req.method || 'POST',
|
|
398
|
+
headers: req.headers || {},
|
|
399
|
+
body: body,
|
|
400
|
+
});
|
|
401
|
+
} catch(e) {
|
|
402
|
+
throw new Error(JSON.stringify({ kind: 'network', message: e.message }));
|
|
403
|
+
}
|
|
404
|
+
const headers = {};
|
|
405
|
+
for (const [k, v] of res.headers.entries()) headers[k] = v;
|
|
406
|
+
const bodyText = await res.text().catch(() => '');
|
|
407
|
+
return { json: JSON.stringify({ bodyText, status: res.status, statusText: res.statusText, headers }) };
|
|
408
|
+
};
|
|
409
|
+
// 8. responsesStreamDrive → JS streamをネイティブリデューサーに流す
|
|
410
|
+
// native responsesStreamDrive は Rust tokio でストリームを読む。
|
|
411
|
+
// JS streamId("js-"で始まる)はRust側にないためクラッシュ → JS実装で代替。
|
|
412
|
+
const _nativeReducerProcessEvent = typeof result.openaiResponsesStreamReducerProcessEvent === 'function'
|
|
413
|
+
? result.openaiResponsesStreamReducerProcessEvent
|
|
414
|
+
: null;
|
|
415
|
+
result.responsesStreamDrive = async function(streamId, reducerId, hasProcessors, onChunkCallback) {
|
|
416
|
+
const st = _jsStreams.get(streamId);
|
|
417
|
+
if (!st) {
|
|
418
|
+
throw new Error(`Native mod HTTP stream was not found: ${streamId}`);
|
|
419
|
+
}
|
|
420
|
+
_jsStreams.delete(streamId);
|
|
421
|
+
let copilotUsage = null;
|
|
422
|
+
for (const event of st.events) {
|
|
423
|
+
if (!_nativeReducerProcessEvent) continue;
|
|
424
|
+
let parsed;
|
|
425
|
+
try { parsed = JSON.parse(_nativeReducerProcessEvent(reducerId, JSON.stringify(event)).json); }
|
|
426
|
+
catch (_) { continue; }
|
|
427
|
+
if (parsed && parsed.copilotUsage !== undefined && parsed.copilotUsage !== null)
|
|
428
|
+
copilotUsage = parsed.copilotUsage;
|
|
429
|
+
const cc = parsed && parsed.chunkContext;
|
|
430
|
+
if (hasProcessors && typeof onChunkCallback === 'function' && cc &&
|
|
431
|
+
(cc.content || cc.messageStart || cc.reportIntentArguments || cc.chunkBoundary || cc.size > 0)) {
|
|
432
|
+
try { onChunkCallback(JSON.stringify(cc)); } catch (_) {}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { json: JSON.stringify({ kind: 'ok', copilotUsage, ttftMs: null, interTokenLatencyMs: null }) };
|
|
436
|
+
};
|
|
437
|
+
// --- end JS model HTTP implementation ---
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// [Android bionic 対応] app.js の oPt() が globalThis.fetch を Rust ベースの B7 に
|
|
443
|
+
// 差し替えるのを阻止する。linuxmusl-arm64/runtime.node の Rust ネットワークスタックは
|
|
444
|
+
// bionic 上の実 I/O で動作しないため、Node.js ビルトイン fetch(動作確認済み)に固定する。
|
|
445
|
+
const _nativeFetch = globalThis.fetch;
|
|
446
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
447
|
+
configurable: true,
|
|
448
|
+
enumerable: true,
|
|
449
|
+
get() { return _nativeFetch; },
|
|
450
|
+
set(_) { /* B7 代入を無視 */ },
|
|
451
|
+
});
|