fix: Windows path bugs, null safety, and URL encoding across client + manager

Client (cc-haha):
- server/api/sessions.ts: use path.basename() instead of split('/').pop()
  for extracting project/repo names on Windows
- server/api/filesystem.ts: use os.tmpdir() and os.homedir() instead of
  hardcoded '/tmp' and process.env.HOME which don't exist on Windows
- utils/plugins/pluginVersioning.ts: split on /[/\]/ for Windows paths
- utils/plugins/loadPluginCommands.ts: handle backslash separators in
  plugin namespace construction
- cli/handlers/autoMode.ts: add optional chaining on response.content
  to prevent crash when API returns null content

Manager (heicode):
- auth/api.ts: fix status always returning 1 regardless of active state
  (was `? 1 : 1`, now `? 1 : 2`)
- users/api.ts, redemption-codes/api.ts, profile/api.ts: use
  URLSearchParams for query string encoding to prevent breakage with
  special characters in search keywords and email addresses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 22:59:26 +08:00
co-authored by Claude Opus 4.6
parent a3c6261d47
commit aa52b1265b
9 changed files with 15 additions and 15 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ export async function autoModeCritiqueHandler(options: {
return
}
const textBlock = response.content.find(block => block.type === 'text')
const textBlock = response.content?.find(block => block.type === 'text')
if (textBlock?.type === 'text') {
process.stdout.write(textBlock.text + '\n')
} else {
+2 -2
View File
@@ -27,7 +27,7 @@ function isAllowedFilesystemPath(targetPath: string): boolean {
const resolvedPath = path.resolve(targetPath)
const homeDir = path.resolve(os.homedir())
if (isWithinRoot(resolvedPath, homeDir) || isWithinRoot(resolvedPath, '/tmp')) {
if (isWithinRoot(resolvedPath, homeDir) || isWithinRoot(resolvedPath, os.tmpdir())) {
return true
}
@@ -95,7 +95,7 @@ async function handleServeFile(url: URL): Promise<Response> {
}
async function handleBrowse(url: URL): Promise<Response> {
const targetPath = url.searchParams.get('path') || process.env.HOME || '/'
const targetPath = url.searchParams.get('path') || os.homedir()
const resolvedPath = path.resolve(targetPath)
if (!isAllowedFilesystemPath(resolvedPath)) {
+3 -3
View File
@@ -12,6 +12,7 @@
* PATCH /api/sessions/:id — 重命名会话
*/
import * as path from 'path'
import { sessionService } from '../services/sessionService.js'
import { conversationService } from '../services/conversationService.js'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
@@ -380,8 +381,7 @@ async function getGitInfo(sessionId: string): Promise<Response> {
repoName = match ? match[1]! : ''
} catch {
// No remote, use directory name
const parts = workDir.split('/')
repoName = parts[parts.length - 1] || ''
repoName = path.basename(workDir) || ''
}
// Get short status
@@ -502,7 +502,7 @@ async function getRecentProjects(url: URL): Promise<Response> {
const entries = Array.from(realPathMap.entries())
const projects = await Promise.all(
entries.map(async ([realPath, info]) => {
const projectName = realPath.split('/').filter(Boolean).pop() || info.projectPath
const projectName = path.basename(realPath) || info.projectPath
let isGit = false
let repoName: string | null = null
@@ -72,9 +72,9 @@ function getCommandNameFromFile(
// Build namespace from parent of skill directory
const relativePath = parentOfSkillDir.startsWith(baseDir)
? parentOfSkillDir.slice(baseDir.length).replace(/^\//, '')
? parentOfSkillDir.slice(baseDir.length).replace(/^[/\\]/, '')
: ''
const namespace = relativePath ? relativePath.split('/').join(':') : ''
const namespace = relativePath ? relativePath.split(/[/\\]/).join(':') : ''
return namespace
? `${pluginName}:${namespace}:${commandBaseName}`
@@ -86,9 +86,9 @@ function getCommandNameFromFile(
// Build namespace from file directory
const relativePath = fileDirectory.startsWith(baseDir)
? fileDirectory.slice(baseDir.length).replace(/^\//, '')
? fileDirectory.slice(baseDir.length).replace(/^[/\\]/, '')
: ''
const namespace = relativePath ? relativePath.split('/').join(':') : ''
const namespace = relativePath ? relativePath.split(/[/\\]/).join(':') : ''
return namespace
? `${pluginName}:${namespace}:${commandBaseName}`
@@ -126,7 +126,7 @@ export function getGitCommitSha(dirPath: string): Promise<string | null> {
*/
export function getVersionFromPath(installPath: string): string | null {
// Versioned paths have format: .../plugins/cache/marketplace/plugin/version/
const parts = installPath.split('/').filter(Boolean)
const parts = installPath.split(/[/\\]/).filter(Boolean)
// Find 'cache' index to determine depth
const cacheIndex = parts.findIndex(
+1 -1
View File
@@ -269,7 +269,7 @@ export async function getHeicodeCurrentUser() {
email: me.data.email || '',
role,
group: me.data.channelId || 'default',
status: me.data.status === 'active' ? 1 : 1,
status: me.data.status === 'active' ? 1 : 2,
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ export async function bindEmail(
email: string,
code: string
): Promise<ApiResponse> {
const res = await api.get(`/api/oauth/email/bind?email=${email}&code=${code}`)
const res = await api.get(`/api/oauth/email/bind?${new URLSearchParams({ email, code })}`)
return res.data
}
+1 -1
View File
@@ -27,7 +27,7 @@ export async function searchRedemptions(
): Promise<GetRedemptionsResponse> {
const { keyword = '', p = 1, page_size = 10 } = params
const res = await api.get(
`/api/redemption/search?keyword=${keyword}&p=${p}&page_size=${page_size}`
`/api/redemption/search?${new URLSearchParams({ keyword, p: String(p), page_size: String(page_size) })}`
)
return res.data
}
+1 -1
View File
@@ -33,7 +33,7 @@ export async function searchUsers(
): Promise<GetUsersResponse> {
const { keyword = '', group = '', p = 1, page_size = 10 } = params
const res = await api.get(
`/api/user/search?keyword=${keyword}&group=${group}&p=${p}&page_size=${page_size}`
`/api/user/search?${new URLSearchParams({ keyword, group, p: String(p), page_size: String(page_size) })}`
)
return res.data
}