Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ NEXT_PUBLIC_LARK_WIKI_URL = https://open-source-bazaar.feishu.cn/wiki/space/7052

NEXT_PUBLIC_LARK_BITABLE_ID = PNOGbGqhPacsHOsvJqHctS77nje
NEXT_PUBLIC_ACTIVITY_TABLE_ID = tblREEMxDOECZZrK
NEXT_PUBLIC_PROJECT_TABLE_ID = tblGnY6Hm0nTSBR9
NEXT_PUBLIC_AWARD_TABLE_ID = tblmYd5V5BMngAp2

NEXT_PUBLIC_STRAPI_API_HOST = https://china-ngo-db.onrender.com/api/
39 changes: 23 additions & 16 deletions .github/scripts/share-reward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,37 @@ interface PRMeta {
assignees: components['schemas']['simple-user'][];
}

const PR_URL = await $`gh api graphql -f query='{
const PR_DATA = await $`gh api graphql -f query='{
repository(owner: "${repositoryOwner}", name: "${repositoryName}") {
issue(number: ${issueNumber}) {
closedByPullRequestsReferences(first: 10) {
nodes {
url
merged
mergeCommit {
oid
}
}
}
}
}
}' --jq '.data.repository.issue.closedByPullRequestsReferences.nodes[] | select(.merged == true) | .url' | head -n 1`;
}' --jq '.data.repository.issue.closedByPullRequestsReferences.nodes[] | select(.merged == true) | {url: .url, mergeCommitSha: .mergeCommit.oid}' | head -n 1`;

if (!PR_URL.text().trim())
throw new ReferenceError('No merged PR is found for the given issue number.');
const prData = PR_DATA.text().trim();

if (!prData) throw new ReferenceError('No merged PR is found for the given issue number.');

const { url: PR_URL, mergeCommitSha } = JSON.parse(prData);

if (!PR_URL || !mergeCommitSha) throw new Error('Missing required fields in PR data');

console.table({ PR_URL, mergeCommitSha });

const { author, assignees }: PRMeta = await (
await $`gh pr view ${PR_URL} --json author,assignees`
).json();

// Function to check if a user is a Copilot/bot user
function isCopilotUser(login: string): boolean {
function isBotUser(login: string) {
const lowerLogin = login.toLowerCase();
return (
lowerLogin.includes('copilot') ||
Expand All @@ -50,19 +59,17 @@ function isCopilotUser(login: string): boolean {
);
}

// Filter out Copilot and bot users from the list
// Filter out Bot users from the list
const allUsers = [author.login, ...assignees.map(({ login }) => login)];
const users = allUsers.filter(login => !isCopilotUser(login));
const users = allUsers.filter(login => !isBotUser(login));

console.log(`All users: ${allUsers.join(', ')}`);
console.log(`Filtered users (excluding bots/copilot): ${users.join(', ')}`);
console.log(`Filtered users (excluding bots): ${users.join(', ')}`);

// Handle case where all users are bots/copilot
if (users.length === 0) {
console.log('No real users found (all users are bots/copilot). Skipping reward distribution.');
console.log(`Filtered users: ${allUsers.join(', ')}`);
process.exit(0);
}
if (!users[0])
throw new ReferenceError(
'No real users found (all users are bots). Skipping reward distribution.',
);

const rewardNumber = parseFloat(reward);

Expand All @@ -86,7 +93,7 @@ console.log(listText);

await $`git config --global user.name "github-actions[bot]"`;
await $`git config --global user.email "github-actions[bot]@users.noreply.github.com"`;
await $`git tag -a "reward-${issueNumber}" -m ${listText}`;
await $`git tag -a "reward-${issueNumber}" ${mergeCommitSha} -m ${listText}`;
await $`git push origin --tags`;

const commentBody = `## Reward data
Expand Down
3 changes: 2 additions & 1 deletion components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
{
title: t('open_source_projects'),
subs: [
{ href: '/project', title: t('open_source_projects') },
{ href: '/project', title: t('self_developed_projects') },
{ href: '/search/project', title: t('bazaar_projects') },
{ href: '/issue', title: 'GitHub issues' },
{ href: '/license-filter', title: t('license_filter') },
{ href: '/finance', title: t('finance_page_title') },
Expand Down
15 changes: 15 additions & 0 deletions components/Project/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FC } from 'react';

import { Project } from '../../models/Project';
import { GitCard } from '../Git/Card';

export const ProjectCard: FC<Project> = ({ name, sourceLink, link, languages, tags, summary }) => (
<GitCard
full_name={name as string}
html_url={sourceLink as string}
homepage={link as string}
languages={languages as string[]}
topics={tags as string[]}
description={summary as string}
/>
Comment on lines +6 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

类型断言存在安全隐患,且缺少 MobX observer 包装。

此组件存在两个问题:

  1. 类型安全问题(关键):所有属性都使用 as stringas string[] 强制类型转换。根据 models/Project.ts 的定义,这些字段的类型是 TableCellValue,它可能是多种类型(number、boolean、array 等)。强制转换会绕过 TypeScript 的类型检查,可能导致运行时错误。

  2. 缺少 MobX observer:参考 ActivityCardOrganizationCard,卡片组件应该用 observer() 包装以响应数据变化。如果 Project 数据是可观察的(observable),当前实现将无法正确更新 UI。

🔧 建议的修复方案

方案 1(推荐):类型守卫 + observer 包装

-import { FC } from 'react';
+import { observer } from 'mobx-react';
+import { FC } from 'react';

 import { Project } from '../../models/Project';
 import { GitCard } from '../Git/Card';

-export const ProjectCard: FC<Project> = ({ name, sourceLink, link, languages, tags, summary }) => (
-  <GitCard
-    full_name={name as string}
-    html_url={sourceLink as string}
-    homepage={link as string}
-    languages={languages as string[]}
-    topics={tags as string[]}
-    description={summary as string}
-  />
-);
+export const ProjectCard: FC<Project> = observer(
+  ({ name, sourceLink, link, languages, tags, summary }) => {
+    const safeString = (value: unknown) => (typeof value === 'string' ? value : '');
+    const safeArray = (value: unknown) => (Array.isArray(value) ? value : []);
+
+    return (
+      <GitCard
+        full_name={safeString(name)}
+        html_url={safeString(sourceLink)}
+        homepage={safeString(link)}
+        languages={safeArray(languages) as string[]}
+        topics={safeArray(tags) as string[]}
+        description={safeString(summary)}
+      />
+    );
+  },
+);

方案 2:在 Project 模型层添加类型转换

如果 TableCellValue 总是返回正确类型,可在 models/Project.ts 中添加类型断言辅助函数,然后在组件中使用这些安全的转换。

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @components/Project/Card.tsx around lines 6 - 14, ProjectCard currently
force-casts all props (name, sourceLink, link, languages, tags, summary) to
string/string[] which bypasses type checks for TableCellValue and can cause
runtime errors; update ProjectCard to perform safe type guards/parsers for each
prop (e.g., check typeof or Array.isArray and coerce/format values only when
valid, handle missing/invalid cases with fallbacks) or call a safe conversion
helper from models/Project.ts that returns the expected string/string[] types,
and wrap the component export with MobX observer() (same pattern used by
ActivityCard and OrganizationCard) so the card reacts to observable changes.

);
29 changes: 29 additions & 0 deletions models/Award.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { BiDataQueryOptions, BiDataTable, BiSearch, TableCellValue } from 'mobx-lark';

import { larkClient } from './Base';
import { AwardTableId, LarkBitableId } from './configuration';

export type Award = Record<
| 'awardName'
| `nominee${'Name' | 'Desc'}`
| 'videoUrl'
| 'reason'
| 'nominator'
| 'createdAt'
| 'votes',
TableCellValue
>;

export class AwardModel extends BiDataTable<Award>() {
client = larkClient;

queryOptions: BiDataQueryOptions = { text_field_as_array: false };

constructor(appId = LarkBitableId, tableId = AwardTableId) {
super(appId, tableId);
}
}

export class SearchAwardModel extends BiSearch<Award>(AwardModel) {
searchKeys = ['awardName', 'nomineeName', 'nomineeDesc', 'reason', 'nominator'];
}
66 changes: 66 additions & 0 deletions models/Project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
BiDataQueryOptions,
BiDataTable,
BiSearch,
TableCellLink,
TableCellValue,
TableRecord,
} from 'mobx-lark';

import { LarkBase, larkClient } from './Base';
import { LarkBitableId, ProjectTableId } from './configuration';

export type Project = LarkBase &
Record<
| 'name'
| 'type'
| 'sourceLink'
| 'link'
| 'license'
| 'languages'
| 'tags'
| 'summary'
| 'logo'
| 'status'
| 'reason',
TableCellValue
>;

export class ProjectModel extends BiDataTable<Project>() {
client = larkClient;

queryOptions: BiDataQueryOptions = { text_field_as_array: false };

constructor(appId = LarkBitableId, tableId = ProjectTableId) {
super(appId, tableId);
}

extractFields({
fields: { sourceLink, link, languages, tags, ...fields },
...meta
}: TableRecord<Project>) {
return {
...meta,
...fields,
sourceLink: (sourceLink as TableCellLink)?.link,
link: (link as TableCellLink)?.link,
languages: languages?.toString().split(/\s*,\s*/) || [],
tags: tags?.toString().split(/\s*,\s*/) || [],
};
}
Comment on lines +38 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

修复字段提取逻辑中的运行时错误风险。

关键问题(第 47-48 行):
languagestagsundefined 时,languages?.toString().split(...) 会抛出 TypeError。可选链只保护了 toString() 调用,但 split() 仍然会在 undefined 上调用导致错误。

次要问题(第 45-46 行):
类型断言后访问 .link 属性缺少可选链,如果 sourceLinklink 不是 TableCellLink 类型,可能返回 undefined

🔧 建议的修复方案
 extractFields({
   fields: { sourceLink, link, languages, tags, ...fields },
   ...meta
 }: TableRecord<Project>) {
   return {
     ...meta,
     ...fields,
-    sourceLink: (sourceLink as TableCellLink)?.link,
-    link: (link as TableCellLink)?.link,
-    languages: languages?.toString().split(/\s*,\s*/) || [],
-    tags: tags?.toString().split(/\s*,\s*/) || [],
+    sourceLink: (sourceLink as TableCellLink | undefined)?.link,
+    link: (link as TableCellLink | undefined)?.link,
+    languages: (languages?.toString() ?? '').split(/\s*,\s*/).filter(Boolean),
+    tags: (tags?.toString() ?? '').split(/\s*,\s*/).filter(Boolean),
   };
 }

或使用更简洁的写法:

-    languages: languages?.toString().split(/\s*,\s*/) || [],
-    tags: tags?.toString().split(/\s*,\s*/) || [],
+    languages: languages ? String(languages).split(/\s*,\s*/) : [],
+    tags: tags ? String(tags).split(/\s*,\s*/) : [],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
extractFields({
fields: { sourceLink, link, languages, tags, ...fields },
...meta
}: TableRecord<Project>) {
return {
...meta,
...fields,
sourceLink: (sourceLink as TableCellLink)?.link,
link: (link as TableCellLink)?.link,
languages: languages?.toString().split(/\s*,\s*/) || [],
tags: tags?.toString().split(/\s*,\s*/) || [],
};
}
extractFields({
fields: { sourceLink, link, languages, tags, ...fields },
...meta
}: TableRecord<Project>) {
return {
...meta,
...fields,
sourceLink: (sourceLink as TableCellLink | undefined)?.link,
link: (link as TableCellLink | undefined)?.link,
languages: (languages?.toString() ?? '').split(/\s*,\s*/).filter(Boolean),
tags: (tags?.toString() ?? '').split(/\s*,\s*/).filter(Boolean),
};
}
🤖 Prompt for AI Agents
In @models/Project.ts around lines 38 - 50, In extractFields, guard against
calling split on undefined and ensure safe access to link properties: for
languages and tags replace languages?.toString().split(...) and
tags?.toString().split(...) with a nullish-safe expression like (languages ??
'').toString().split(/\s*,\s*/).filter(Boolean) and (tags ??
'').toString().split(/\s*,\s*/).filter(Boolean); and ensure sourceLink and link
use optional chaining on the casted TableCellLink (e.g. (sourceLink as
TableCellLink)?.link and (link as TableCellLink)?.link) so accessing .link
cannot throw if the value is not a TableCellLink.

}

export class SearchProjectModel extends BiSearch<Project>(ProjectModel) {
searchKeys = [
'name',
'type',
'sourceLink',
'link',
'license',
'languages',
'tags',
'summary',
'status',
'reason',
];
}
4 changes: 4 additions & 0 deletions models/System.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { BaseModel, DataObject, Filter, ListModel, toggle } from 'mobx-restful';
import { Constructor } from 'web-utility';

import { SearchActivityModel } from './Activity';
import { SearchAwardModel } from './Award';
import { ownClient } from './Base';
import { OrganizationModel } from './Organization';
import { SearchProjectModel } from './Project';

export type SearchableFilter<D extends DataObject> = Filter<D> & {
keywords?: string;
Expand All @@ -22,6 +24,8 @@ export type CityCoordinateMap = Record<string, [number, number]>;
export class SystemModel extends BaseModel {
searchMap = {
activity: SearchActivityModel,
project: SearchProjectModel,
award: SearchAwardModel,
NGO: OrganizationModel,
} as Record<string, Constructor<SearchModel<DataObject>>>;

Expand Down
4 changes: 3 additions & 1 deletion models/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ export const LarkWikiDomain = hostname;
export const LarkWikiId = pathname.split('/').pop()!;

export const LarkBitableId = process.env.NEXT_PUBLIC_LARK_BITABLE_ID!,
ActivityTableId = process.env.NEXT_PUBLIC_ACTIVITY_TABLE_ID!;
ActivityTableId = process.env.NEXT_PUBLIC_ACTIVITY_TABLE_ID!,
ProjectTableId = process.env.NEXT_PUBLIC_PROJECT_TABLE_ID!,
AwardTableId = process.env.NEXT_PUBLIC_AWARD_TABLE_ID!;
Comment on lines +38 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Step 1: Check if .env.example exists and its contents
echo "=== Looking for environment configuration files ==="
fd -t f '\.(env|example)' | head -20

echo ""
echo "=== Checking .env.example for the mentioned variables ==="
if [ -f ".env.example" ]; then
  grep -n "ACTIVITY_TABLE_ID\|PROJECT_TABLE_ID\|AWARD_TABLE_ID" .env.example || echo "Variables not found in .env.example"
else
  echo ".env.example not found"
fi

echo ""
echo "=== Checking README files ==="
fd -t f 'README' -i | head -10

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 255


🏁 Script executed:

#!/bin/bash
# Step 2: Read models/configuration.ts to understand the context
echo "=== Reading models/configuration.ts ==="
wc -l models/configuration.ts

echo ""
echo "=== Content around lines 38-40 ==="
sed -n '30,50p' models/configuration.ts

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 667


🏁 Script executed:

#!/bin/bash
# Step 3: Search for usage of these table IDs in the codebase
echo "=== Searching for usage of AwardTableId ==="
rg "AwardTableId" -A 2 -B 2 | head -30

echo ""
echo "=== Searching for usage of ProjectTableId ==="
rg "ProjectTableId" -A 2 -B 2 | head -30

echo ""
echo "=== Searching for usage of ActivityTableId ==="
rg "ActivityTableId" -A 2 -B 2 | head -30

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 2683


🏁 Script executed:

#!/bin/bash
# Step 4: Check Award.ts and Project.ts for context
echo "=== Checking Award.ts ==="
fd -name "Award.ts" | head -5

echo ""
echo "=== Checking Project.ts ==="
fd -name "Project.ts" | head -5

# Read Award.ts if it exists
if [ -f "models/Award.ts" ]; then
  echo "=== First 50 lines of models/Award.ts ==="
  head -50 models/Award.ts
fi

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 1424


需要在项目中补充环境变量文档。

ActivityTableIdProjectTableIdAwardTableId 这三个常量使用了非空断言 !,意味着对应的环境变量(NEXT_PUBLIC_ACTIVITY_TABLE_IDNEXT_PUBLIC_PROJECT_TABLE_IDNEXT_PUBLIC_AWARD_TABLE_ID)是运行时必需的。目前项目中不存在 .env.example 文件,这些环境变量的配置要求无法被记录,新开发者将无法知道需要设置哪些变量,容易导致运行时错误。

建议创建或更新 .env.example 文件,列出所有必需的环境变量及其说明,确保项目配置的透明度和可维护性。

🤖 Prompt for AI Agents
In @models/configuration.ts around lines 38 - 40, Add a .env.example documenting
the required env vars referenced in models/configuration.ts: list
NEXT_PUBLIC_ACTIVITY_TABLE_ID, NEXT_PUBLIC_PROJECT_TABLE_ID,
NEXT_PUBLIC_AWARD_TABLE_ID with short descriptions and example values so new
devs know they are required for ActivityTableId, ProjectTableId, and
AwardTableId; optionally mention runtime requirement in README and remove or
guard non-null assertions in ActivityTableId/ProjectTableId/AwardTableId if you
prefer runtime-safe errors.

16 changes: 16 additions & 0 deletions pages/award/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cache, compose, errorLogger } from 'next-ssr-middleware';
import { FC } from 'react';

import { Award, AwardModel } from '../../models/Award';

export const getServerSideProps = compose(cache(), errorLogger, async () => {
const awards = await new AwardModel().getAll();

return { props: { awards } };
});

const AwardPage: FC<{ awards: Award[] }> = ({ awards }) => {
return <></>;
};

export default AwardPage;
3 changes: 3 additions & 0 deletions pages/search/[model]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CardPage, CardPageProps } from '../../../components/Layout/CardPage';
import { PageHead } from '../../../components/Layout/PageHead';
import { SearchBar } from '../../../components/Navigator/SearchBar';
import { OrganizationCard } from '../../../components/Organization/Card';
import { ProjectCard } from '../../../components/Project/Card';
import systemStore, { SearchPageMeta } from '../../../models/System';
import { i18n, I18nContext } from '../../../models/Translation';

Expand Down Expand Up @@ -40,11 +41,13 @@ export const getServerSideProps = compose<{ model: string }, SearchModelPageProp

const SearchNameMap = ({ t }: typeof i18n): Record<string, string> => ({
activity: t('activity'),
project: t('open_source_projects'),
NGO: t('NGO'),
});

const SearchCardMap: Record<string, CardPageProps['Card']> = {
activity: ActivityCard,
project: ProjectCard,
NGO: OrganizationCard,
};

Expand Down
2 changes: 2 additions & 0 deletions translation/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export default {
hackathon: 'Hackathon',
bounty: 'Open Source Bounty',
open_source_projects: 'Open Source projects',
self_developed_projects: 'Self-developed projects',
bazaar_projects: 'Bazaar projects',
open_source_bazaar: 'Open Source Bazaar',
home_page: 'Home Page',
wiki: 'Wiki',
Expand Down
2 changes: 2 additions & 0 deletions translation/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export default {
join_us: '参与',
open_collaborator_award: '开放协作人奖',
open_source_projects: '开源项目',
self_developed_projects: '自研项目',
bazaar_projects: '市集项目',
activity: '活动',
hackathon: '黑客马拉松',
bounty: '开源悬赏',
Expand Down
2 changes: 2 additions & 0 deletions translation/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export default {
join_us: '參與',
open_collaborator_award: '開放協作人獎',
open_source_projects: '開源項目',
self_developed_projects: '自研項目',
bazaar_projects: '市集項目',
activity: '活動',
hackathon: '黑客馬拉松',
bounty: '開源懸賞',
Expand Down