Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
391 changes: 75 additions & 316 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,31 @@
"dev": "tsc --watch",
"test": "node --test dist/tests/*.test.js",
"setup": "node dist/main.js setup",
"daemon": "bash scripts/daemon.sh",
"daemon": "node scripts/daemon.js",
"daemon:unix": "bash scripts/daemon.sh",
"daemon:win": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/daemon.ps1",
"visualize": "npx tsx src/tools/visualize-logs.ts"
},
"dependencies": {
"qrcode": "^1.5.4",
"cross-spawn": "^7.0.6",
"qrcode": "^1.5.4",
"qrcode-terminal": "^0.12.0"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/node": "^22.0.0",
"@types/qrcode": "^1.5.6",
"@types/qrcode-terminal": "^0.12.0",
"typescript": "^5.7.0"
},
"keywords": ["wechat", "claude-code", "claude", "bridge", "chat", "skill"],
"keywords": [
"wechat",
"claude-code",
"claude",
"bridge",
"chat",
"skill"
],
"author": "Wechat-ggGitHub",
"license": "MIT",
"repository": {
Expand Down
259 changes: 259 additions & 0 deletions scripts/daemon.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
param(
[Parameter(Position = 0)]
[ValidateSet("setup", "start", "stop", "restart", "status", "logs")]
[string]$Command = "status",

[Parameter(Position = 1)]
[ValidatePattern("^[A-Za-z0-9_-]+$")]
[string]$Instance = "default"
)

$ErrorActionPreference = "Stop"

$ProjectDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path

# default 实例继续使用原来的数据目录,保留当前已登录账号
$BaseDataDir = Join-Path $env:USERPROFILE ".wechat-claude-code"

if ($Instance -eq "default") {
$DataDir = $BaseDataDir
} else {
$InstancesDir = Join-Path $BaseDataDir "instances"
$DataDir = Join-Path $InstancesDir $Instance
}

# Node.js 子进程会继承这个环境变量
$env:WCC_DATA_DIR = $DataDir

$LogDir = Join-Path $DataDir "logs"
$PidFile = Join-Path $DataDir "wechat-claude-code.pid"
$StdoutLog = Join-Path $LogDir "stdout.log"
$StderrLog = Join-Path $LogDir "stderr.log"
$EntryFile = Join-Path $ProjectDir "dist\main.js"

function Get-NodePath {
$nodeCommand = Get-Command node -ErrorAction Stop
return $nodeCommand.Source
}

function Get-SavedProcessId {
if (-not (Test-Path $PidFile)) {
return $null
}

try {
$value = (Get-Content $PidFile -Raw).Trim()
[int]$parsedId = 0

if ([int]::TryParse($value, [ref]$parsedId)) {
return $parsedId
}
} catch {
return $null
}

return $null
}

function Test-ProcessRunning {
param(
[int]$ProcessId
)

if ($ProcessId -le 0) {
return $false
}

try {
Get-Process -Id $ProcessId -ErrorAction Stop | Out-Null
return $true
} catch {
return $false
}
}

function Remove-PidFile {
if (Test-Path $PidFile) {
Remove-Item $PidFile -Force -ErrorAction SilentlyContinue
}
}

function Invoke-InstanceSetup {
if (-not (Test-Path $EntryFile)) {
throw "dist/main.js not found. Run 'npm run build' first."
}

New-Item -ItemType Directory -Force -Path $DataDir | Out-Null

$nodePath = Get-NodePath

Write-Host "Setting up instance: $Instance"
Write-Host "Data directory: $DataDir"
Write-Host ""

Push-Location $ProjectDir

try {
& $nodePath $EntryFile setup

if ($LASTEXITCODE -ne 0) {
throw "Setup failed with exit code $LASTEXITCODE"
}
} finally {
Pop-Location
}
}

function Start-Daemon {
$savedPid = Get-SavedProcessId

if ($savedPid -and (Test-ProcessRunning -ProcessId $savedPid)) {
Write-Host "Instance '$Instance' is already running (PID: $savedPid)"
return
}

Remove-PidFile

if (-not (Test-Path $EntryFile)) {
throw "dist/main.js not found. Run 'npm run build' first."
}

New-Item -ItemType Directory -Force -Path $LogDir | Out-Null

$nodePath = Get-NodePath

Write-Host "Starting instance '$Instance'..."
Write-Host "Data directory: $DataDir"

$process = Start-Process `
-FilePath $nodePath `
-ArgumentList @("`"$EntryFile`"", "start") `
-WorkingDirectory $ProjectDir `
-WindowStyle Hidden `
-RedirectStandardOutput $StdoutLog `
-RedirectStandardError $StderrLog `
-PassThru

Set-Content -Path $PidFile -Value $process.Id -Encoding ASCII

Start-Sleep -Milliseconds 800

if (-not (Test-ProcessRunning -ProcessId $process.Id)) {
Remove-PidFile

Write-Host ""
Write-Host "Daemon exited immediately."

if (Test-Path $StderrLog) {
Write-Host "=== stderr.log ==="
Get-Content $StderrLog -Encoding UTF8 -Tail 50
}

throw "Failed to start instance '$Instance'"
}

Write-Host "Started instance '$Instance' (PID: $($process.Id))"
Write-Host "Logs: $StdoutLog"
}

function Stop-Daemon {
$savedPid = Get-SavedProcessId

if (-not $savedPid) {
Write-Host "Instance '$Instance' is not running (no PID file)"
Remove-PidFile
return
}

if (-not (Test-ProcessRunning -ProcessId $savedPid)) {
Write-Host "Instance '$Instance' is not running (stale PID: $savedPid)"
Remove-PidFile
return
}

Write-Host "Stopping instance '$Instance' (PID: $savedPid)..."

& taskkill.exe /PID $savedPid /T /F | Out-Host

Start-Sleep -Milliseconds 500
Remove-PidFile

Write-Host "Stopped instance '$Instance'"
}

function Show-Status {
$savedPid = Get-SavedProcessId

Write-Host "Instance: $Instance"
Write-Host "Data directory: $DataDir"

if ($savedPid -and (Test-ProcessRunning -ProcessId $savedPid)) {
$process = Get-Process -Id $savedPid
$uptime = (Get-Date) - $process.StartTime

Write-Host "Status: Running"
Write-Host "PID: $savedPid"
Write-Host "Started: $($process.StartTime)"
Write-Host "Uptime: $([math]::Floor($uptime.TotalHours))h $($uptime.Minutes)m"
return
}

if ($savedPid) {
Remove-PidFile
}

Write-Host "Status: Not running"
}

function Show-Logs {
Write-Host "Instance: $Instance"
Write-Host "Data directory: $DataDir"
Write-Host ""

$found = $false

if (Test-Path $StdoutLog) {
$found = $true
Write-Host "=== stdout.log ==="
Get-Content $StdoutLog -Encoding UTF8 -Tail 100
}

if (Test-Path $StderrLog) {
$found = $true
Write-Host ""
Write-Host "=== stderr.log ==="
Get-Content $StderrLog -Encoding UTF8 -Tail 100
}

if (-not $found) {
Write-Host "No logs found"
}
}

switch ($Command) {
"setup" {
Invoke-InstanceSetup
}

"start" {
Start-Daemon
}

"stop" {
Stop-Daemon
}

"restart" {
Stop-Daemon
Start-Sleep -Seconds 1
Start-Daemon
}

"status" {
Show-Status
}

"logs" {
Show-Logs
}
}
6 changes: 4 additions & 2 deletions src/claude/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawn, type ChildProcess } from 'node:child_process';
import crossSpawn from 'cross-spawn';
import type { ChildProcess } from 'node:child_process';
import { writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
Expand Down Expand Up @@ -121,10 +122,11 @@ export async function claudeQuery(options: QueryOptions): Promise<QueryResult> {
};

try {
child = spawn('claude', args, {
child = crossSpawn('claude', args, {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
windowsHide: true,
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
Expand Down
5 changes: 2 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { DEFAULT_WORKING_DIR } from "./constants.js";
import { DATA_DIR, DEFAULT_WORKING_DIR } from "./constants.js";

export interface Config {
workingDirectory: string;
model?: string;
systemPrompt?: string;
}

const CONFIG_DIR = join(homedir(), ".wechat-claude-code");
const CONFIG_DIR = DATA_DIR;
const CONFIG_PATH = join(CONFIG_DIR, "config.json");

const DEFAULT_CONFIG: Config = {
Expand Down
4 changes: 2 additions & 2 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { mkdirSync, appendFileSync, readdirSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { DATA_DIR } from "./constants.js";

const LOG_DIR = join(homedir(), ".wechat-claude-code", "logs");
const LOG_DIR = join(DATA_DIR, "logs");
const MAX_LOG_FILES = 30; // Keep at most 30 days of logs

/** Clean up old log files beyond MAX_LOG_FILES retention. */
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ async function handleMessage(
accountId: account.accountId,
session,
updateSession,
clearSession: () => sessionStore.clear(account.accountId),
clearSession: () => sessionStore.clear(account.accountId, session),
getChatHistoryText: (limit?: number) => sessionStore.getChatHistoryText(session, limit),
text: userText,
};
Expand Down
4 changes: 2 additions & 2 deletions src/wechat/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { join } from 'node:path';
import { homedir } from 'node:os';
import { readdirSync, statSync } from 'node:fs';
import { loadJson, saveJson, validateAccountId } from '../store.js';
import { logger } from '../logger.js';
import { DATA_DIR } from '../constants.js';

export const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';

Expand All @@ -14,7 +14,7 @@ export interface AccountData {
createdAt: string;
}

const ACCOUNTS_DIR = join(homedir(), '.wechat-claude-code', 'accounts');
const ACCOUNTS_DIR = join(DATA_DIR, 'accounts');

function accountPath(accountId: string): string {
validateAccountId(accountId);
Expand Down